Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'

[FIX/BE][US#75/TASK#119] Align chickin seeder and DeleteOne with CreateOne behavior, create project lookup API

See merge request mbugroup/lti-api!37
This commit is contained in:
Hafizh A. Y.
2025-10-23 04:24:04 +00:00
54 changed files with 2442 additions and 684 deletions
@@ -0,0 +1,106 @@
package repository
import (
"context"
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ApprovalRepository interface {
BaseRepository[entity.Approval]
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
}
type approvalRepositoryImpl struct {
*BaseRepositoryImpl[entity.Approval]
}
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
return &approvalRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.Approval](db),
}
}
func (r *approvalRepositoryImpl) FindByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
var approvals []entity.Approval
q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil {
return nil, err
}
return approvals, nil
}
func (r *approvalRepositoryImpl) LatestByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
var approval entity.Approval
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
if err := q.Limit(1).First(&approval).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &approval, nil
}
func (r *approvalRepositoryImpl) LatestByTargets(
ctx context.Context,
workflow string,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]entity.Approval, error) {
if len(approvableIDs) == 0 {
return nil, nil
}
result := make(map[uint]entity.Approval, len(approvableIDs))
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
var approvals []entity.Approval
if err := q.Find(&approvals).Error; err != nil {
return nil, err
}
for _, approval := range approvals {
if _, exists := result[approval.ApprovableId]; exists {
continue
}
result[approval.ApprovableId] = approval
}
return result, nil
}
@@ -0,0 +1,234 @@
package service
import (
"context"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
type ApprovalService interface {
RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
}
type approvalService struct {
repo commonRepo.ApprovalRepository
}
func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService {
return &approvalService{repo: repo}
}
func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error {
return approvalutils.RegisterWorkflowSteps(workflow, steps)
}
func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string {
return approvalutils.WorkflowSteps(workflow)
}
func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) {
return approvalutils.ApprovalStepName(workflow, step)
}
func (s *approvalService) CreateApproval(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
step approvalutils.ApprovalStep,
action *entity.ApprovalAction,
actorID uint,
note *string,
) (*entity.Approval, error) {
record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note)
if err != nil {
return nil, err
}
if err := s.repo.CreateOne(ctx, record, nil); err != nil {
return nil, err
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) List(
ctx context.Context,
module string,
approvableID *uint,
page, limit int,
search string,
) ([]entity.Approval, int64, error) {
module = strings.TrimSpace(strings.ToUpper(module))
search = strings.TrimSpace(search)
if limit <= 0 {
limit = 10
}
if page <= 0 {
page = 1
}
offset := (page - 1) * limit
records, total, err := s.repo.GetAll(
ctx,
offset,
limit,
func(db *gorm.DB) *gorm.DB {
query := db.
Where("approvable_type = ?", module).
Order("action_at DESC").
Preload("ActionUser")
if approvableID != nil {
query = query.Where("approvable_id = ?", *approvableID)
}
if search != "" {
like := "%" + strings.ToLower(search) + "%"
query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like)
}
return query
},
)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, 0, nil
}
return nil, 0, err
}
if len(records) == 0 {
return nil, total, nil
}
workflow := approvalutils.ApprovalWorkflowKey(module)
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, total, nil
}
func (s *approvalService) ListByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, nil
}
func (s *approvalService) LatestByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if record == nil {
return nil, nil
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) LatestByTargets(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]*entity.Approval, error) {
records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if len(records) == 0 {
return nil, nil
}
result := make(map[uint]*entity.Approval, len(records))
for approvableID, approval := range records {
approvalCopy := approval
s.decorateApproval(workflow, &approvalCopy)
result[approvableID] = &approvalCopy
}
return result, nil
}
func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) {
if approval == nil {
return
}
currentName := strings.TrimSpace(approval.StepName)
if currentName == "" {
if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok {
approval.StepName = name
}
} else {
approval.StepName = currentName
}
}
func (s *approvalService) isApprovalTableMissing(err error) bool {
if err == nil {
return false
}
errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "no such table: approvals") {
return true
}
schemaIssues := []string{
`relation "approvals" does not exist`,
`column "step_name" does not exist`,
`column "step_number" does not exist`,
`column "action" does not exist`,
`column "status" does not exist`,
`column "step" does not exist`,
}
for _, issue := range schemaIssues {
if strings.Contains(errMsg, issue) {
return true
}
}
return false
}
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS approvals_approvable_lookup;
DROP TABLE IF EXISTS approvals;
@@ -0,0 +1,12 @@
CREATE TABLE approvals (
id BIGSERIAL PRIMARY KEY,
approvable_type VARCHAR(50) NOT NULL,
approvable_id BIGINT NOT NULL,
step SMALLINT NOT NULL,
status VARCHAR(20) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id);
@@ -0,0 +1,18 @@
ALTER TABLE approvals
RENAME COLUMN action TO status;
UPDATE approvals
SET status = 'PENDING'
WHERE status IS NULL;
ALTER TABLE approvals
ALTER COLUMN status SET NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step_number TO step;
ALTER TABLE approvals
DROP COLUMN step_name;
ALTER TABLE approvals
RENAME COLUMN action_at TO created_at;
@@ -0,0 +1,14 @@
ALTER TABLE approvals
RENAME COLUMN status TO action;
ALTER TABLE approvals
ALTER COLUMN action DROP NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step TO step_number;
ALTER TABLE approvals
ADD COLUMN step_name VARCHAR NOT NULL;
ALTER TABLE approvals
RENAME COLUMN created_at TO action_at;
@@ -0,0 +1,25 @@
BEGIN;
-- Recreate legacy columns on project_flock_kandangs
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE detached_at IS NULL;
-- Restore product_category_id reference and drop category column
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS category;
COMMIT;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -0,0 +1,43 @@
BEGIN;
-- Add category column to project_flocks and backfill existing rows
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
UPDATE project_flocks
SET category = 'GROWING'
WHERE category IS NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET NOT NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET DEFAULT 'GROWING';
-- Drop legacy foreign key reference and column
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS product_category_id;
-- Simplify project_flock_kandangs structure
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS assigned_at,
DROP COLUMN IF EXISTS detached_at,
DROP COLUMN IF EXISTS updated_at;
ALTER TABLE project_flock_kandangs
ALTER COLUMN created_at SET DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
ON project_flock_kandangs (project_flock_id, kandang_id);
COMMIT;
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickin_details;
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS project_chickin_details (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_project_chickin_id
FOREIGN KEY (project_chickin_id)
REFERENCES project_chickins(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
@@ -1 +0,0 @@
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -1,3 +0,0 @@
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
+154 -26
View File
@@ -8,6 +8,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -50,7 +51,7 @@ func Run(db *gorm.DB) error {
return err return err
} }
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, productCategories, fcrs, locations) projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
if err != nil { if err != nil {
return err return err
} }
@@ -242,12 +243,12 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCategories, fcrs, locations map[string]uint) (map[string]uint, error) { func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Key string Key string
Flock string Flock string
Area string Area string
ProductCategory string Category utils.ProjectFlockCategory
Fcr string Fcr string
Location string Location string
Period int Period int
@@ -256,7 +257,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
Key: "Singaparna Period 1", Key: "Singaparna Period 1",
Flock: "Flock Priangan", Flock: "Flock Priangan",
Area: "Priangan", Area: "Priangan",
ProductCategory: "Day Old Chick", Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer", Fcr: "FCR Layer",
Location: "Singaparna", Location: "Singaparna",
Period: 1, Period: 1,
@@ -265,7 +266,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
Key: "Cikaum Period 1", Key: "Cikaum Period 1",
Flock: "Flock Banten", Flock: "Flock Banten",
Area: "Banten", Area: "Banten",
ProductCategory: "Day Old Chick", Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer", Fcr: "FCR Layer",
Location: "Cikaum", Location: "Cikaum",
Period: 1, Period: 1,
@@ -283,10 +284,6 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
if !ok { if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area) return nil, fmt.Errorf("area %s not seeded", seed.Area)
} }
categoryID, ok := productCategories[seed.ProductCategory]
if !ok {
return nil, fmt.Errorf("product category %s not seeded", seed.ProductCategory)
}
fcrID, ok := fcrs[seed.Fcr] fcrID, ok := fcrs[seed.Fcr]
if !ok { if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr) return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
@@ -297,13 +294,13 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
} }
var projectFlock entity.ProjectFlock var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND product_category_id = ? AND fcr_id = ? AND location_id = ? AND period = ?", err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, categoryID, fcrID, locationID, seed.Period).First(&projectFlock).Error flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{ projectFlock = entity.ProjectFlock{
FlockId: flockID, FlockId: flockID,
AreaId: areaID, AreaId: areaID,
ProductCategoryId: categoryID, Category: string(seed.Category),
FcrId: fcrID, FcrId: fcrID,
LocationId: locationID, LocationId: locationID,
Period: seed.Period, Period: seed.Period,
@@ -318,7 +315,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{ if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": string(seed.Category),
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"period": seed.Period, "period": seed.Period,
@@ -326,12 +323,68 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
return nil, err return nil, err
} }
} }
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return nil, err
}
result[seed.Key] = projectFlock.Id result[seed.Key] = projectFlock.Id
} }
return result, nil return result, nil
} }
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
if projectFlockID == 0 || actorID == 0 {
return nil
}
workflow := utils.ApprovalWorkflowProjectFlock.String()
steps := []struct {
step approvalutils.ApprovalStep
action entity.ApprovalAction
}{
{step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated},
{step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved},
}
for _, cfg := range steps {
var count int64
if err := tx.Model(&entity.Approval{}).
Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step]
if !ok || strings.TrimSpace(stepName) == "" {
stepName = fmt.Sprintf("Step %d", cfg.step)
}
var actionPtr *entity.ApprovalAction
action := cfg.action
actionPtr = &action
record := entity.Approval{
ApprovableType: workflow,
ApprovableId: projectFlockID,
StepNumber: uint16(cfg.step),
StepName: stepName,
Action: actionPtr,
ActionBy: uintPtr(actorID),
}
if err := tx.Create(&record).Error; err != nil {
return err
}
}
return nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) { func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -341,9 +394,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
ProjectFlockKey *string ProjectFlockKey *string
}{ }{
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, {Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")}, {Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")},
{Name: "Cikaum 2", Status: utils.KandangStatusPengajuan, Location: "Cikaum", PicKey: "admin"}, {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
} }
result := make(map[string]uint, len(seeds)) result := make(map[string]uint, len(seeds))
@@ -381,7 +434,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if err := tx.Create(&kandang).Error; err != nil { if err := tx.Create(&kandang).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil { if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err return nil, err
} }
} else if err != nil { } else if err != nil {
@@ -400,7 +453,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil { if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err return nil, err
} }
} }
@@ -410,25 +463,24 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil return result, nil
} }
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint, createdBy uint) error { func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil { if err := detachActivePivot(tx, kandangID); err != nil {
return err return err
} }
if projectFlockID == nil { if projectFlockID == nil {
return nil return nil
} }
return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy) return ensureActivePivot(tx, *projectFlockID, kandangID)
} }
func detachActivePivot(tx *gorm.DB, kandangID uint) error { func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Model(&entity.ProjectFlockKandang{}). return tx.Where("kandang_id = ?", kandangID).
Where("kandang_id = ? AND detached_at IS NULL", kandangID). Delete(&entity.ProjectFlockKandang{}).Error
Updates(map[string]any{"detached_at": time.Now()}).Error
} }
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error { func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ? AND detached_at IS NULL", projectFlockID, kandangID). err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error First(&pivot).Error
if err == nil { if err == nil {
return nil return nil
@@ -439,7 +491,6 @@ func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) e
newRecord := entity.ProjectFlockKandang{ newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: kandangID, KandangId: kandangID,
CreatedBy: createdBy,
} }
return tx.Create(&newRecord).Error return tx.Create(&newRecord).Error
} }
@@ -1119,7 +1170,6 @@ func seedChickin(tx *gorm.DB, createdBy uint) error {
return err return err
} }
// Update/Insert ProjectFlockPopulation
var population entity.ProjectFlockPopulation var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1147,6 +1197,84 @@ func seedChickin(tx *gorm.DB, createdBy uint) error {
return err return err
} }
} }
var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details
continue
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
var productWarehouses []entity.ProductWarehouse
err = tx.Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return err
}
// If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 {
continue
}
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0
for _, pw := range productWarehouses {
totalQty += pw.Quantity
}
if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err
}
chickin.Quantity = totalQty
}
for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&detail).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err
}
}
}
// zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err
}
}
} }
return nil return nil
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectChickinDetail struct {
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"column:project_chickin_id;not null"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Quantity float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy uint `gorm:"column:created_by;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type ApprovalAction string
const (
ApprovalActionApproved ApprovalAction = "APPROVED"
ApprovalActionRejected ApprovalAction = "REJECTED"
ApprovalActionCreated ApprovalAction = "CREATED"
ApprovalActionUpdated ApprovalAction = "UPDATED"
)
type Approval struct {
Id uint `gorm:"primaryKey"`
ApprovableType string `gorm:"size:50;not null;index:approvals_approvable_lookup,priority:1"`
ApprovableId uint `gorm:"not null;index:approvals_approvable_lookup,priority:2"`
StepNumber uint16 `gorm:"not null"`
StepName string `gorm:"not null"`
Action *ApprovalAction `gorm:"type:VARCHAR(20)"`
Notes *string `gorm:"type:text"`
ActionAt time.Time `gorm:"autoCreateTime"`
ActionBy *uint `gorm:"index"`
ActionUser *User `gorm:"foreignKey:ActionBy;references:Id"`
}
-18
View File
@@ -1,18 +0,0 @@
package entities
import (
"time"
)
type AuditLog struct {
Id uint `gorm:"primaryKey"`
TableName string `gorm:"size:100;not null"`
RecordId uint `gorm:"not null"`
Action string `gorm:"size:30;not null"`
BeforeData string `gorm:"type:jsonb"`
AfterData string `gorm:"type:jsonb"`
ChangedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
User *User `gorm:"foreignKey:ChangedBy;references:Id"`
}
-29
View File
@@ -1,29 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlock struct {
Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"`
AreaId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlock struct {
Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+2 -7
View File
@@ -4,14 +4,9 @@ import "time"
type ProjectFlockKandang struct { type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_active,priority:1,where:detached_at IS NULL"` ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_active,priority:2,where:detached_at IS NULL"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedBy uint `gorm:"not null"`
AssignedAt time.Time `gorm:"autoCreateTime"`
DetachedAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
-26
View File
@@ -1,26 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
EntityTypeProjectFlockKandang = "PROJECT_FLOCK_KANDANG"
)
type StockAvailability struct {
Id uint `gorm:"primaryKey"`
EntityType string `gorm:"size:50;not null"`
EntityId uint `gorm:"not null"`
ProductId uint `gorm:"not null"`
Quantity float64 `gorm:"not null;default:0"`
ReservedQuantity float64 `gorm:"not null;default:0"`
Unit string `gorm:"size:20"`
LastUpdated time.Time `gorm:"autoUpdateTime"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
}
@@ -0,0 +1,100 @@
package controller
import (
"math"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
)
type ApprovalController struct {
ApprovalService common.ApprovalService
}
func NewApprovalController(approvalService common.ApprovalService) *ApprovalController {
return &ApprovalController{
ApprovalService: approvalService,
}
}
func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
moduleName := strings.TrimSpace(c.Query("module_name", ""))
if moduleName == "" {
return fiber.NewError(fiber.StatusBadRequest, "`module_name` is required")
}
moduleIDParam := strings.TrimSpace(c.Query("module_id", ""))
var moduleID *uint
if moduleIDParam != "" {
value, err := strconv.ParseUint(moduleIDParam, 10, 64)
if err != nil || value == 0 {
return fiber.NewError(fiber.StatusBadRequest, "module_id must be a positive integer")
}
id := uint(value)
moduleID = &id
}
groupByStep := c.QueryBool("group_step_number", false)
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10)
search := strings.TrimSpace(c.Query("search", ""))
query := &validation.Query{
ModuleName: moduleName,
ModuleId: moduleID,
GroupByStep: groupByStep,
Page: page,
Limit: limit,
Search: search,
}
records, totalResults, err := u.ApprovalService.List(
c.Context(),
query.ModuleName,
query.ModuleId,
query.Page,
query.Limit,
query.Search,
)
if err != nil {
return err
}
if query.GroupByStep {
data := dto.ToApprovalGroupDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalGroupDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: data,
})
}
flat := dto.ToApprovalDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: flat,
})
}
@@ -0,0 +1,122 @@
package dto
import (
"sort"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
)
type ApprovalBaseDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Action *string `json:"action"`
Notes *string `json:"notes"`
ActionBy userDTO.UserBaseDTO `json:"action_by"`
ActionAt time.Time `json:"action_at"`
}
type ApprovalGroupDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Approvals []ApprovalBaseDTO `json:"approvals"`
}
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{
Notes: e.Notes,
}
if e.StepNumber > 0 {
stepCopy := uint16(e.StepNumber)
dto.StepNumber = stepCopy
}
stepName := strings.TrimSpace(e.StepName)
if stepName == "" && e.ApprovableType != "" && e.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(e.ApprovableType), approvalutils.ApprovalStep(e.StepNumber)); ok {
stepName = label
}
}
dto.StepName = stepName
if e.Action != nil {
value := strings.TrimSpace(string(*e.Action))
if value != "" {
valueCopy := value
dto.Action = &valueCopy
}
}
if e.ActionUser != nil && e.ActionUser.Id != 0 {
user := userDTO.ToUserBaseDTO(*e.ActionUser)
dto.ActionBy = user
} else if e.ActionBy != nil && *e.ActionBy != 0 {
dto.ActionBy = userDTO.UserBaseDTO{
Id: *e.ActionBy,
IdUser: int64(*e.ActionBy),
}
}
if !e.ActionAt.IsZero() {
at := e.ActionAt
dto.ActionAt = at
}
return dto
}
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
result := make([]ApprovalBaseDTO, len(items))
for i, item := range items {
result[i] = ToApprovalDTO(item)
}
return result
}
func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
if len(items) == 0 {
return nil
}
type groupAccumulator struct {
StepName string
Approvals []ApprovalBaseDTO
}
groups := make(map[uint16]*groupAccumulator)
order := make([]uint16, 0)
for _, item := range items {
step := item.StepNumber
acc, exists := groups[step]
if !exists {
stepName := strings.TrimSpace(item.StepName)
if stepName == "" && item.ApprovableType != "" && item.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(item.ApprovableType), approvalutils.ApprovalStep(item.StepNumber)); ok {
stepName = label
}
}
acc = &groupAccumulator{StepName: stepName}
groups[step] = acc
order = append(order, step)
}
acc.Approvals = append(acc.Approvals, ToApprovalDTO(item))
}
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
result := make([]ApprovalGroupDTO, len(order))
for i, step := range order {
acc := groups[step]
result[i] = ApprovalGroupDTO{
StepNumber: step,
StepName: acc.StepName,
Approvals: acc.Approvals,
}
}
return result
}
+25
View File
@@ -0,0 +1,25 @@
package approvals
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ApprovalModule struct{}
func (ApprovalModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
approvalRepo := commonRepo.NewApprovalRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
userService := sUser.NewUserService(userRepo, validate)
ApprovalRoutes(router, userService, approvalService)
}
+19
View File
@@ -0,0 +1,19 @@
package approvals
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u
ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals")
route.Get("/", ctrl.GetAll)
}
@@ -0,0 +1,10 @@
package validation
type Query struct {
ModuleName string `json:"module_name" validate:"required_strict"`
ModuleId *uint `json:"module_id,omitempty" validate:"omitempty,gt=0"`
GroupByStep bool `json:"group_by_step"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -1,9 +1,13 @@
package repository package repository
import ( import (
"sort"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
for f := range utils.AllFlagTypes() { for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f)) flagList = append(flagList, string(f))
} }
sort.Strings(flagList)
type approvalStepConstant struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
}
workflowConstants := approvalutils.WorkflowConstants()
workflowKeys := make([]string, 0, len(workflowConstants))
for key := range workflowConstants {
workflowKeys = append(workflowKeys, key)
}
sort.Strings(workflowKeys)
approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys))
for _, key := range workflowKeys {
stepMap := workflowConstants[key]
if len(stepMap) == 0 {
continue
}
stepList := make([]approvalStepConstant, 0, len(stepMap))
for stepStr, label := range stepMap {
stepNum, err := strconv.ParseUint(stepStr, 10, 16)
if err != nil || stepNum == 0 {
continue
}
stepList = append(stepList, approvalStepConstant{
StepNumber: uint16(stepNum),
StepName: label,
})
}
if len(stepList) == 0 {
continue
}
sort.Slice(stepList, func(i, j int) bool {
return stepList[i].StepNumber < stepList[j].StepNumber
})
approvalWorkflows = append(approvalWorkflows, map[string]interface{}{
"key": key,
"steps": stepList,
})
}
return map[string]interface{}{ return map[string]interface{}{
"flags": flagList, "flags": flagList,
@@ -42,5 +90,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"BISNIS", "BISNIS",
"INDIVIDUAL", "INDIVIDUAL",
}, },
"approval_workflows": approvalWorkflows,
} }
} }
@@ -266,6 +266,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id) s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// create stock log for decrease (source)
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
decreaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeDecrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeQty,
AfterQuantity: sourcePW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: sourcePW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
return err
}
// Tambah stok di gudang tujuan // Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
@@ -296,6 +314,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id) s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
// create stock log for increase (destination)
beforeDestQty := destPW.Quantity - product.ProductQty
increaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeIncrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeDestQty,
AfterQuantity: destPW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: destPW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err)
return err
}
} }
return nil return nil
@@ -1,21 +1,30 @@
package repository package repository
import ( import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
type FlockRepository interface { type FlockRepository interface {
repository.BaseRepository[entity.Flock] repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
} }
type FlockRepositoryImpl struct { type FlockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Flock] *repository.BaseRepositoryImpl[entity.Flock]
db *gorm.DB
} }
func NewFlockRepository(db *gorm.DB) FlockRepository { func NewFlockRepository(db *gorm.DB) FlockRepository {
return &FlockRepositoryImpl{ return &FlockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db),
db: db,
} }
} }
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
}
@@ -2,6 +2,8 @@ package service
import ( import (
"errors" "errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
@@ -79,8 +81,22 @@ func (s *flockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.
return nil, err return nil, err
} }
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name is required")
}
exists, err := s.Repository.NameExists(c.Context(), name, nil)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
createBody := &entity.Flock{ createBody := &entity.Flock{
Name: req.Name, Name: name,
CreatedBy: 1, CreatedBy: 1,
} }
@@ -100,7 +116,20 @@ func (s flockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (
updateBody := make(map[string]any) updateBody := make(map[string]any)
if req.Name != nil { if req.Name != nil {
updateBody["name"] = *req.Name name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
exists, err := s.Repository.NameExists(c.Context(), name, &id)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
updateBody["name"] = name
} }
if len(updateBody) == 0 { if len(updateBody) == 0 {
@@ -17,6 +17,7 @@ type KandangRepository interface {
ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error) ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error)
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
} }
type KandangRepositoryImpl struct { type KandangRepositoryImpl struct {
@@ -81,3 +82,10 @@ func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
} }
return kandang, nil return kandang, nil
} }
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
return r.db.WithContext(ctx).
Model(&entity.Kandang{}).
Where("project_flock_id = ?", projectFlockID).
Update("status", string(status)).Error
}
@@ -9,7 +9,6 @@ import (
flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productCategoryBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -26,8 +25,8 @@ type ChickinBaseDTO struct {
type ProjectFlockDTO struct { type ProjectFlockDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
Flock *flockBaseDTO.FlockBaseDTO `json:"flock"` Flock *flockBaseDTO.FlockBaseDTO `json:"flock"`
ProductCategory *productCategoryBaseDTO.ProductCategoryBaseDTO `json:"product_category"`
Area *areaBaseDTO.AreaBaseDTO `json:"area"` Area *areaBaseDTO.AreaBaseDTO `json:"area"`
Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"` Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"`
Location *locationBaseDTO.LocationBaseDTO `json:"location"` Location *locationBaseDTO.LocationBaseDTO `json:"location"`
@@ -71,11 +70,6 @@ func ToFlockDTO(e entity.Flock) flockBaseDTO.FlockBaseDTO {
func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO { func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO {
return kandangBaseDTO.ToKandangBaseDTO(e) return kandangBaseDTO.ToKandangBaseDTO(e)
} }
func ToProductCategoryDTO(e entity.ProductCategory) productCategoryBaseDTO.ProductCategoryBaseDTO {
return productCategoryBaseDTO.ToProductCategoryBaseDTO(e)
}
func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO { func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO {
return areaBaseDTO.ToAreaBaseDTO(e) return areaBaseDTO.ToAreaBaseDTO(e)
} }
@@ -98,11 +92,6 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock)
flock = &mapped flock = &mapped
} }
var productCategory *productCategoryBaseDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryBaseDTO.ToProductCategoryBaseDTO(e.ProductCategory)
productCategory = &mapped
}
var area *areaBaseDTO.AreaBaseDTO var area *areaBaseDTO.AreaBaseDTO
if e.Area.Id != 0 { if e.Area.Id != 0 {
mapped := areaBaseDTO.ToAreaBaseDTO(e.Area) mapped := areaBaseDTO.ToAreaBaseDTO(e.Area)
@@ -121,8 +110,8 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
return ProjectFlockDTO{ return ProjectFlockDTO{
Id: e.Id, Id: e.Id,
Period: e.Period, Period: e.Period,
Category: e.Category,
Flock: flock, Flock: flock,
ProductCategory: productCategory,
Area: area, Area: area,
Fcr: fcr, Fcr: fcr,
Location: location, Location: location,
@@ -10,7 +10,6 @@ import (
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
rAuditLog "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -22,8 +21,9 @@ type ChickinModule struct{}
func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
kandangRepo := rKandang.NewKandangRepository(db) kandangRepo := rKandang.NewKandangRepository(db)
auditlogrepo := rAuditLog.NewAuditLogRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
@@ -32,7 +32,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, projectflockpopulationrepo, validate) chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ChickinRoutes(router, userService, chickinService) ChickinRoutes(router, userService, chickinService)
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail]
}
type ChickinDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectChickinDetail]
}
func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
}
}
@@ -8,11 +8,8 @@ import (
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
AuditLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@@ -38,12 +35,12 @@ type chickinService struct {
WarehouseRepo rWarehouse.WarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
ProjectFlockRepo rProjectFlock.ProjectflockRepository ProjectFlockRepo rProjectFlock.ProjectflockRepository
AuditLogRepo AuditLogRepo.AuditLogRepository ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
} }
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, validate *validator.Validate) ChickinService { func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate) ChickinService {
return &chickinService{ return &chickinService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -52,9 +49,9 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
AuditLogRepo: auditLogRepo,
ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectflockPopulationRepo: projectflockpopulationRepo,
ProjectChickinDetailRepo: projectChickinDetailRepo,
} }
} }
@@ -67,7 +64,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.Kandang.Pic").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.Flock"). Preload("ProjectFlockKandang.ProjectFlock.Flock").
Preload("ProjectFlockKandang.ProjectFlock.ProductCategory").
Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Area").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location").
@@ -113,7 +109,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), 1) projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err) s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return nil, err return nil, err
@@ -125,23 +121,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
projectFlock, err := s.ProjectFlockRepo.GetByID(
c.Context(),
projectflockkandang.ProjectFlockId,
func(db *gorm.DB) *gorm.DB {
return db.Preload("ProductCategory")
},
)
if err != nil {
s.Log.Errorf("Failed to get project flock: %+v", err)
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
var productWarehouses []entity.ProductWarehouse var productWarehouses []entity.ProductWarehouse
err = s.ProductWarehouseRepo.DB(). err = s.ProductWarehouseRepo.DB().
WithContext(c.Context()). WithContext(c.Context()).
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("created_at DESC"). Order("created_at DESC").
Find(&productWarehouses).Error Find(&productWarehouses).Error
if err != nil { if err != nil {
@@ -169,7 +154,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
} }
newChickin := &entity.ProjectChickin{ newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: projectflockkandang.ProjectFlockId, ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate, ChickInDate: chickinDate,
Quantity: totalQuantity, Quantity: totalQuantity,
Note: "", Note: "",
@@ -190,6 +175,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err return nil, err
} }
// add ke detail chickin
newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin detail: %+v", err)
return nil, err
}
} }
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
@@ -250,85 +248,142 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
// todo: cek apakah chickin sudah di approve atau belum db := s.Repository.DB()
chickin, err := s.Repository.GetByID(c.Context(), id, nil) tx := db.WithContext(c.Context()).Begin()
if tx.Error != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
return tx.Error
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
}
return err
}
chickinRepoTx := s.Repository.WithTx(tx)
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found") return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err) s.Log.Errorf("Failed get chickin by id: %+v", err)
return err return rollback(err)
} }
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) var population entity.ProjectFlockPopulation
if err != nil { if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return err
}
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found") return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
} }
s.Log.Errorf("Failed to delete chickin: %+v", err) s.Log.Errorf("Failed to get project flock population: %+v", err)
return err return rollback(err)
} }
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) newReserved := population.ReservedQuantity - chickin.Quantity
if newReserved < 0 {
newReserved = 0
}
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return rollback(err)
}
// helper: restore quantities from details; returns (restored bool, error)
restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
return false, err
}
if len(details) == 0 {
return false, nil
}
for _, d := range details {
var pw entity.ProductWarehouse
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return false, err
}
updatedQuantity := pw.Quantity + d.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err
}
}
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
return false, err
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
return rollback(err)
}
if !restored {
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err) s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return err return rollback(err)
}
var warehouse entity.Warehouse
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
} }
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
if err != nil {
s.Log.Errorf("Failed to get warehouse: %+v", err) s.Log.Errorf("Failed to get warehouse: %+v", err)
return err return rollback(err)
} }
projectFlock, err := s.ProjectFlockRepo.GetByID(
c.Context(),
projectflockkandang.ProjectFlockId,
func(db *gorm.DB) *gorm.DB {
return db.Preload("ProductCategory")
},
)
if err != nil {
s.Log.Errorf("Failed to get project flock: %+v", err)
return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). err = tx.WithContext(c.Context()).Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("created_at DESC"). Order("product_warehouses.created_at DESC").
First(&productWarehouse).Error First(&productWarehouse).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
} }
s.Log.Errorf("Failed to get product warehouse: %+v", err) s.Log.Errorf("Failed to get product warehouse: %+v", err)
return err return rollback(err)
} }
updatedQuantity := productWarehouse.Quantity + chickin.Quantity updatedQuantity := productWarehouse.Quantity + chickin.Quantity
err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{ if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
"quantity": updatedQuantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err return rollback(err)
}
}
// delete chickin (single place)
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
s.Log.Errorf("Failed to delete chickin: %+v", err)
return rollback(err)
}
if err := tx.Commit().Error; err != nil {
s.Log.Errorf("Failed to commit transaction: %+v", err)
return rollback(err)
} }
return nil return nil
@@ -190,6 +190,37 @@ func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
}) })
} }
func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
results, err := u.ProjectflockService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit projectflock approval successfully"
)
if len(results) == 1 {
data = dto.ToProjectFlockListDTO(results[0])
} else {
message = "Submit projectflock approvals successfully"
data = dto.ToProjectFlockListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
param := c.Params("flock_id") param := c.Params("flock_id")
@@ -213,3 +244,19 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
Data: responseBody, Data: responseBody,
}) })
} }
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
projectFlockIdStr := c.Query("project_flock_id", "")
kandangIdStr := c.Query("kandang_id", "")
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{Code: fiber.StatusOK,
Status: "success",
Message: "Get projectflock kandang successfully",
Data: dto.ToProjectFlockKandangDTO(*result)})
}
@@ -4,83 +4,42 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
) )
type ProjectFlockBaseDTO struct { type ProjectFlockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
// FlockId uint `json:"flock_id"`
// AreaId uint `json:"area_id"`
// ProductCategoryId uint `json:"product_category_id"`
// FcrId uint `json:"fcr_id"`
// LocationId uint `json:"location_id"`
Period int `json:"period"` Period int `json:"period"`
} }
func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{
Id: e.Id,
// FlockId: e.FlockId,
// AreaId: e.AreaId,
// ProductCategoryId: e.ProductCategoryId,
// FcrId: e.FcrId,
// LocationId: e.LocationId,
Period: e.Period,
}
}
type FlockSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductCategorySummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
type FcrSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type KandangSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type ProjectFlockListDTO struct { type ProjectFlockListDTO struct {
ProjectFlockBaseDTO ProjectFlockBaseDTO
Flock *FlockSummaryDTO `json:"flock,omitempty"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *AreaSummaryDTO `json:"area,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
ProductCategory *ProductCategorySummaryDTO `json:"product_category,omitempty"` Category string `json:"category"`
Fcr *FcrSummaryDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *LocationSummaryDTO `json:"location,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangSummaryDTO `json:"kandangs,omitempty"` Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
} }
type ProjectFlockDetailDTO struct { type ProjectFlockDetailDTO struct {
ProjectFlockListDTO ProjectFlockListDTO
} }
type FlockPeriodSummaryDTO struct { type FlockPeriodDTO struct {
Flock FlockSummaryDTO `json:"flock"` Flock flockDTO.FlockBaseDTO `json:"flock"`
NextPeriod int `json:"next_period"` NextPeriod int `json:"next_period"`
} }
@@ -91,66 +50,56 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped createdUser = &mapped
} }
var flockSummary *FlockSummaryDTO var kandangSummaries []kandangDTO.KandangBaseDTO
if e.Flock.Id != 0 { if len(e.Kandangs) > 0 {
summary := ToFlockSummaryDTO(e.Flock) kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
flockSummary = &summary
}
var areaSummary *AreaSummaryDTO
if e.Area.Id != 0 {
areaSummary = &AreaSummaryDTO{
Id: e.Area.Id,
Name: e.Area.Name,
}
}
var categorySummary *ProductCategorySummaryDTO
if e.ProductCategory.Id != 0 {
categorySummary = &ProductCategorySummaryDTO{
Id: e.ProductCategory.Id,
Name: e.ProductCategory.Name,
Code: e.ProductCategory.Code,
}
}
var fcrSummary *FcrSummaryDTO
if e.Fcr.Id != 0 {
fcrSummary = &FcrSummaryDTO{
Id: e.Fcr.Id,
Name: e.Fcr.Name,
}
}
var locationSummary *LocationSummaryDTO
if e.Location.Id != 0 {
locationSummary = &LocationSummaryDTO{
Id: e.Location.Id,
Name: e.Location.Name,
Address: e.Location.Address,
}
}
kandangSummaries := make([]KandangSummaryDTO, len(e.Kandangs))
for i, kandang := range e.Kandangs { for i, kandang := range e.Kandangs {
kandangSummaries[i] = KandangSummaryDTO{ kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
} }
} }
var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flockSummary = &mapped
}
var areaSummary *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
areaSummary = &mapped
}
var fcrSummary *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcrSummary = &mapped
}
var locationSummary *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
locationSummary = &mapped
}
latestApproval := defaultProjectFlockLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return ProjectFlockListDTO{ return ProjectFlockListDTO{
ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary, Flock: flockSummary,
Area: areaSummary, Area: areaSummary,
ProductCategory: categorySummary, Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary, Fcr: fcrSummary,
Location: locationSummary, Location: locationSummary,
Kandangs: kandangSummaries,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
Approval: latestApproval,
} }
} }
@@ -168,15 +117,47 @@ func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO {
} }
} }
func ToFlockSummaryDTO(e entity.Flock) FlockSummaryDTO { func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.ApprovalBaseDTO {
return FlockSummaryDTO{ result := approvalDTO.ApprovalBaseDTO{}
step := utils.ProjectFlockStepPengajuan
if step > 0 {
result.StepNumber = uint16(step)
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, step); ok {
result.StepName = label
} else if label, ok := utils.ProjectFlockApprovalSteps[step]; ok {
result.StepName = label
}
}
if e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser)
} else if e.CreatedBy != 0 {
result.ActionBy = userDTO.UserBaseDTO{
Id: e.CreatedBy,
IdUser: int64(e.CreatedBy),
}
}
return result
}
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
return ProjectFlockBaseDTO{
Id: e.Id,
Period: e.Period,
}
}
func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockBaseDTO {
return flockDTO.FlockBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
} }
} }
func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO { func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodDTO {
return FlockPeriodSummaryDTO{ return FlockPeriodDTO{
Flock: ToFlockSummaryDTO(flock), Flock: ToFlockSummaryDTO(flock),
NextPeriod: next, NextPeriod: next,
} }
@@ -0,0 +1,108 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// internal DTO used only for lookup response: project flock with kandangs carrying pivot ids
type KandangWithPivotDTO struct {
kandangDTO.KandangBaseDTO
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"`
}
type ProjectFlockWithPivotDTO struct {
ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
}
type ProjectFlockKandangDTO struct {
Id uint `json:"id"`
ProjectFlockId uint `json:"project_flock_id"`
KandangId uint `json:"kandang_id"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
}
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(e.Kandang)
kandang = &mapped
}
var pf *ProjectFlockWithPivotDTO
if e.ProjectFlock.Id != 0 {
// build project flock with kandangs that include pivot ids
pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period,
},
Category: e.ProjectFlock.Category,
}
// fill related small summaries
if e.ProjectFlock.Flock.Id != 0 {
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
pfLocal.Flock = &mapped
}
if e.ProjectFlock.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
pfLocal.Area = &mapped
}
if e.ProjectFlock.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.ProjectFlock.Fcr)
pfLocal.Fcr = &mapped
}
if e.ProjectFlock.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.ProjectFlock.Location)
pfLocal.Location = &mapped
}
if e.ProjectFlock.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser)
pfLocal.CreatedUser = &mapped
}
// build pivot map
pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id
}
// populate kandangs with pivot ids
for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k)
var pid *uint
if v, ok := pivotMap[k.Id]; ok {
vv := v
pid = &vv
}
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
KandangBaseDTO: kb,
ProjectFlockKandangId: pid,
})
}
pf = &pfLocal
}
return ProjectFlockKandangDTO{
Id: e.Id,
ProjectFlockId: e.ProjectFlockId,
KandangId: e.KandangId,
Kandang: kandang,
ProjectFlock: pf,
}
}
@@ -1,8 +1,12 @@
package project_flocks package project_flocks
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm" "gorm.io/gorm"
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
@@ -10,6 +14,7 @@ import (
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -24,7 +29,13 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, validate) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlock, utils.ProjectFlockApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
}
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService) ProjectflockRoutes(router, userService, projectflockService)
@@ -2,7 +2,6 @@ package repository
import ( import (
"context" "context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
@@ -10,8 +9,9 @@ import (
type ProjectFlockKandangRepository interface { type ProjectFlockKandangRepository interface {
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
@@ -32,14 +32,13 @@ func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, reco
return r.db.WithContext(ctx).Create(&records).Error return r.db.WithContext(ctx).Create(&records).Error
} }
func (r *projectFlockKandangRepositoryImpl) MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error { func (r *projectFlockKandangRepositoryImpl) DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error {
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil return nil
} }
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Where("project_flock_id = ? AND kandang_id IN ? AND detached_at IS NULL", projectFlockID, kandangIDs). Delete(&entity.ProjectFlockKandang{}).Error
Updates(map[string]any{"detached_at": detachedAt}).Error
} }
func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) { func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) {
@@ -47,9 +46,14 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock"). Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
Preload("CreatedUser"). Order("project_flock_id ASC, created_at ASC").
Order("project_flock_id ASC, assigned_at ASC").
Find(&records).Error; err != nil { Find(&records).Error; err != nil {
return nil, err return nil, err
} }
@@ -69,10 +73,34 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock"). Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
Preload("CreatedUser").
First(record, id).Error; err != nil { First(record, id).Error; err != nil {
return nil, err return nil, err
} }
return record, nil return record, nil
} }
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
First(record).Error; err != nil {
return nil, err
}
return record, nil
}
@@ -25,5 +25,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
} }
@@ -4,17 +4,18 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -28,7 +29,9 @@ type ProjectflockService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error)
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
} }
type projectflockService struct { type projectflockService struct {
@@ -38,6 +41,8 @@ type projectflockService struct {
FlockRepo flockRepository.FlockRepository FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository KandangRepo kandangRepository.KandangRepository
PivotRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
type FlockPeriodSummary struct { type FlockPeriodSummary struct {
@@ -50,6 +55,7 @@ func NewProjectflockService(
flockRepo flockRepository.FlockRepository, flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository, kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository, pivotRepo repository.ProjectFlockKandangRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate, validate *validator.Validate,
) ProjectflockService { ) ProjectflockService {
return &projectflockService{ return &projectflockService{
@@ -59,6 +65,8 @@ func NewProjectflockService(
FlockRepo: flockRepo, FlockRepo: flockRepo,
KandangRepo: kandangRepo, KandangRepo: kandangRepo,
PivotRepo: pivotRepo, PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
} }
@@ -67,7 +75,6 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Flock"). Preload("Flock").
Preload("Area"). Preload("Area").
Preload("ProductCategory").
Preload("Fcr"). Preload("Fcr").
Preload("Location"). Preload("Location").
Preload("Kandangs") Preload("Kandangs")
@@ -115,15 +122,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
db = db. db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id"). Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN product_categories ON product_categories.id = project_flocks.product_category_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(` Where(`
LOWER(flocks.name) LIKE ? LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ? OR LOWER(areas.name) LIKE ?
OR LOWER(product_categories.name) LIKE ? OR LOWER(project_flocks.category) LIKE ?
OR LOWER(product_categories.code) LIKE ?
OR LOWER(fcrs.name) LIKE ? OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ? OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ? OR LOWER(locations.address) LIKE ?
@@ -146,7 +151,6 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery,
) )
} }
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) { for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
@@ -159,6 +163,27 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
s.Log.Errorf("Failed to get projectflocks: %+v", err) s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, err return nil, 0, err
} }
if s.ApprovalSvc != nil && len(projectflocks) > 0 {
ids := make([]uint, len(projectflocks))
for i, item := range projectflocks {
ids[i] = item.Id
}
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err)
} else if len(latestMap) > 0 {
for i := range projectflocks {
if approval, ok := latestMap[projectflocks[i].Id]; ok {
projectflocks[i].LatestApproval = approval
}
}
}
}
return projectflocks, total, nil return projectflocks, total, nil
} }
@@ -171,6 +196,23 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock
s.Log.Errorf("Failed get projectflock by id: %+v", err) s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, err return nil, err
} }
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 {
if projectflock.LatestApproval == nil {
latest := approvals[len(approvals)-1]
projectflock.LatestApproval = &latest
}
} else {
projectflock.LatestApproval = nil
}
}
return projectflock, nil return projectflock, nil
} }
@@ -179,16 +221,20 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
cat := strings.ToUpper(req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
if len(req.KandangIds) == 0 { if len(req.KandangIds) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
} }
if err := common.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryId, Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB())}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -210,31 +256,48 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
} }
} }
tx := s.Repository.DB().Begin()
if tx.Error != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
projectRepo := repository.NewProjectflockRepository(tx)
nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
if err != nil {
tx.Rollback()
s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period")
}
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId, FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
ProductCategoryId: req.ProductCategoryId, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
LocationId: req.LocationId, LocationId: req.LocationId,
Period: nextPeriod,
CreatedBy: 1, CreatedBy: 1,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
if err != nil {
return err
}
createBody.Period = period
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
tx.Rollback() return err
}
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil {
return err
}
actorID := uint(1) //TODO: Change From Auth
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err = approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
createBody.Id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
)
return err
})
if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) { if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
} }
@@ -242,17 +305,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs, createBody.CreatedBy); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err)
return nil, err
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
return s.GetOne(c, createBody.Id) return s.GetOne(c, createBody.Id)
} }
@@ -269,13 +321,14 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err) s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
updateBody := make(map[string]any) updateBody := make(map[string]any)
var relationChecks []common.RelationCheck hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck
if req.FlockId != nil { if req.FlockId != nil {
updateBody["flock_id"] = *req.FlockId updateBody["flock_id"] = *req.FlockId
relationChecks = append(relationChecks, common.RelationCheck{ hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Flock", Name: "Flock",
ID: req.FlockId, ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
@@ -283,23 +336,25 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
if req.AreaId != nil { if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
relationChecks = append(relationChecks, common.RelationCheck{ hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area", Name: "Area",
ID: req.AreaId, ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()), Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
}) })
} }
if req.ProductCategoryId != nil { if req.Category != nil {
updateBody["product_category_id"] = *req.ProductCategoryId cat := strings.ToUpper(*req.Category)
relationChecks = append(relationChecks, common.RelationCheck{ if !utils.IsValidProjectFlockCategory(cat) {
Name: "Product category", return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
ID: req.ProductCategoryId, }
Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB()),
}) updateBody["category"] = cat
} }
if req.FcrId != nil { if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId updateBody["fcr_id"] = *req.FcrId
relationChecks = append(relationChecks, common.RelationCheck{ hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR", Name: "FCR",
ID: req.FcrId, ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
@@ -307,24 +362,24 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
if req.LocationId != nil { if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId updateBody["location_id"] = *req.LocationId
relationChecks = append(relationChecks, common.RelationCheck{ hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location", Name: "Location",
ID: req.LocationId, ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()), Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
}) })
} }
if req.Period != nil {
updateBody["period"] = *req.Period
}
if len(relationChecks) > 0 { if len(relationChecks) > 0 {
if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil { if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
return nil, err return nil, err
} }
} }
var newKandangIDs []uint var newKandangIDs []uint
hasKandangChanges := false
if req.KandangIds != nil { if req.KandangIds != nil {
hasKandangChanges = true
if len(req.KandangIds) == 0 { if len(req.KandangIds) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
} }
@@ -346,20 +401,21 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
} }
tx := s.Repository.DB().Begin() hasChanges := hasBodyChanges || hasKandangChanges
if tx.Error != nil { if !hasChanges {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") return s.GetOne(c, id)
} }
projectRepo := repository.NewProjectflockRepository(tx) err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
tx.Rollback() return err
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
s.Log.Errorf("Failed to update projectflock: %+v", err) } else {
return nil, err if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
return err
} }
} }
@@ -369,49 +425,181 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
existingIDs[k.Id] = struct{}{} existingIDs[k.Id] = struct{}{}
} }
newSet := make(map[uint]struct{}, len(newKandangIDs)) newSet := make(map[uint]struct{}, len(newKandangIDs))
for _, id := range newKandangIDs { for _, kid := range newKandangIDs {
newSet[id] = struct{}{} newSet[kid] = struct{}{}
} }
var toDetach []uint var toDetach []uint
for id := range existingIDs { for kid := range existingIDs {
if _, ok := newSet[id]; !ok { if _, ok := newSet[kid]; !ok {
toDetach = append(toDetach, id) toDetach = append(toDetach, kid)
} }
} }
var toAttach []uint var toAttach []uint
for id := range newSet { for kid := range newSet {
if _, ok := existingIDs[id]; !ok { if _, ok := existingIDs[kid]; !ok {
toAttach = append(toAttach, id) toAttach = append(toAttach, kid)
} }
} }
if len(toDetach) > 0 { if len(toDetach) > 0 {
if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil { if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil {
tx.Rollback() return err
s.Log.Errorf("Failed to detach kandangs from projectflock %d: %+v", id, err)
return nil, err
} }
} }
if len(toAttach) > 0 { if len(toAttach) > 0 {
if err := s.attachKandangs(c.Context(), tx, id, toAttach, existing.CreatedBy); err != nil { if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil {
tx.Rollback() return err
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err)
return nil, err
} }
} }
} }
if err := tx.Commit().Error; err != nil { if hasChanges {
tx.Rollback() actorID := uint(1) //TODO: Change From Auth
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if approvalSvc != nil {
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
if err != nil {
return err
}
shouldRecordUpdate := latestBeforeReset == nil ||
latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
latestBeforeReset.Action == nil ||
(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)
if shouldRecordUpdate {
action := entity.ApprovalActionUpdated
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
); err != nil {
return err
}
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
return nil, err
} }
return s.GetOne(c, id) return s.GetOne(c, id)
} }
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := uniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.ProjectFlockStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockStepAktif
}
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
}
return err
}
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusActive,
); err != nil {
return err
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusNonActive,
); err != nil {
return err
}
}
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
project, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *project)
}
return updated, nil
}
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -422,40 +610,88 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
tx := s.Repository.DB().Begin() err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if tx.Error != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
if len(existing.Kandangs) > 0 { if len(existing.Kandangs) > 0 {
ids := make([]uint, len(existing.Kandangs)) ids := make([]uint, len(existing.Kandangs))
for i, k := range existing.Kandangs { for i, k := range existing.Kandangs {
ids[i] = k.Id ids[i] = k.Id
} }
if err := s.detachKandangs(c.Context(), tx, id, ids, true); err != nil { if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to detach kandangs before deleting projectflock %d: %+v", id, err)
return err return err
} }
} }
if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
} }
s.Log.Errorf("Failed to delete projectflock: %+v", err)
return err return err
} }
if err := tx.Commit().Error; err != nil { return nil
tx.Rollback() })
return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return fiberErr
}
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
return err
} }
return nil return nil
} }
func (s projectflockService) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) {
// keep for backward compatibility; delegate to new consolidated method
return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
}
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
return pfk, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" {
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
return pfk, nil
}
if projectFlockIdStr == "" || kandangIdStr == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
}
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kid, err := strconv.Atoi(kandangIdStr)
if err != nil || kid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
}
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB { flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser") return db.Preload("CreatedUser")
@@ -534,24 +770,26 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s
} }
} }
func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, createdBy uint) error { func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil return nil
} }
if err := tx.Model(&entity.Kandang{}). if err := dbTransaction.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs). Where("id IN ?", kandangIDs).
Updates(map[string]any{"project_flock_id": projectFlockID}).Error; err != nil { Updates(map[string]any{
"project_flock_id": projectFlockID,
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
} }
pivotRepo := s.pivotRepoWithTx(tx) pivotRepo := s.pivotRepoWithTx(dbTransaction)
records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
for i, id := range kandangIDs { for i, id := range kandangIDs {
records[i] = &entity.ProjectFlockKandang{ records[i] = &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: id, KandangId: id,
CreatedBy: createdBy,
} }
} }
if err := pivotRepo.CreateMany(ctx, records); err != nil { if err := pivotRepo.CreateMany(ctx, records); err != nil {
@@ -560,7 +798,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr
return nil return nil
} }
func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error {
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil return nil
} }
@@ -570,21 +808,21 @@ func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, pr
updates["status"] = string(utils.KandangStatusNonActive) updates["status"] = string(utils.KandangStatusNonActive)
} }
if err := tx.Model(&entity.Kandang{}). if err := dbTransaction.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs). Where("id IN ?", kandangIDs).
Updates(updates).Error; err != nil { Updates(updates).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
} }
if err := s.pivotRepoWithTx(tx).MarkDetached(ctx, projectFlockID, kandangIDs, time.Now()); err != nil { if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
} }
return nil return nil
} }
func (s projectflockService) pivotRepoWithTx(tx *gorm.DB) repository.ProjectFlockKandangRepository { func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.PivotRepo == nil { if s.PivotRepo == nil {
return repository.NewProjectFlockKandangRepository(tx) return repository.NewProjectFlockKandangRepository(dbTransaction)
} }
return s.PivotRepo.WithTx(tx) return s.PivotRepo.WithTx(dbTransaction)
} }
@@ -3,7 +3,7 @@ package validation
type Create struct { type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
ProductCategoryId uint `json:"product_category_id" validate:"required_strict,number,gt=0"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
@@ -12,10 +12,9 @@ type Create struct {
type Update struct { type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
ProductCategoryId *uint `json:"product_category_id,omitempty" validate:"omitempty,number,gt=0"` Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"`
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
} }
@@ -23,10 +22,16 @@ type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"` SortBy string `query:"sort_by" validate:"omitempty"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"` Period int `query:"period" validate:"omitempty,number,gt=0"`
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
} }
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -1,21 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type AuditLogRepository interface {
repository.BaseRepository[entity.AuditLog]
}
type AuditLogRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.AuditLog]
}
func NewAuditLogRepository(db *gorm.DB) AuditLogRepository {
return &AuditLogRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.AuditLog](db),
}
}
@@ -1,21 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockAvailabilityRepository interface {
repository.BaseRepository[entity.StockAvailability]
}
type StockAvailabilityRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockAvailability]
}
func NewStockAvailabilityRepository(db *gorm.DB) StockAvailabilityRepository {
return &StockAvailabilityRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockAvailability](db),
}
}
+2
View File
@@ -13,6 +13,7 @@ import (
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -28,6 +29,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
constants.ConstantModule{}, constants.ConstantModule{},
inventory.InventoryModule{}, inventory.InventoryModule{},
production.ProductionModule{}, production.ProductionModule{},
approvals.ApprovalModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
@@ -0,0 +1,243 @@
package approvals
import (
"errors"
"fmt"
"strings"
"sync"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
type ApprovalStep uint16
type ApprovalWorkflowKey string
func (k ApprovalWorkflowKey) String() string {
return string(k)
}
type NextStepCallback func(current ApprovalStep, decision entity.ApprovalAction) (ApprovalStep, bool)
var (
approvalActions = map[entity.ApprovalAction]struct{}{
entity.ApprovalActionApproved: {},
entity.ApprovalActionRejected: {},
entity.ApprovalActionCreated: {},
entity.ApprovalActionUpdated: {},
}
approvalWorkflows = make(map[ApprovalWorkflowKey]map[ApprovalStep]string)
approvalWorkflowsMu sync.RWMutex
)
// WorkflowConstants prepares the registered workflows for exposure via constants endpoints.
func WorkflowConstants() map[string]map[string]string {
approvalWorkflowsMu.RLock()
defer approvalWorkflowsMu.RUnlock()
if len(approvalWorkflows) == 0 {
return nil
}
result := make(map[string]map[string]string, len(approvalWorkflows))
for workflow, steps := range approvalWorkflows {
if len(steps) == 0 {
continue
}
stepMap := make(map[string]string, len(steps))
for step, label := range steps {
stepMap[fmt.Sprintf("%d", step)] = label
}
result[workflow.String()] = stepMap
}
if len(result) == 0 {
return nil
}
return result
}
// RegisterWorkflowSteps stores the available steps for a workflow key (usually matching approvable type).
func RegisterWorkflowSteps(workflow ApprovalWorkflowKey, steps map[ApprovalStep]string) error {
workflowStr := strings.TrimSpace(workflow.String())
if workflowStr == "" {
return errors.New("workflow key is required")
}
if len(steps) == 0 {
return fmt.Errorf("no steps defined for workflow %q", workflowStr)
}
copied := make(map[ApprovalStep]string, len(steps))
for step, label := range steps {
if step == 0 {
return fmt.Errorf("workflow %q contains step 0 which is not allowed", workflowStr)
}
trimmed := strings.TrimSpace(label)
if trimmed == "" {
return fmt.Errorf("workflow %q contains empty label for step %d", workflowStr, step)
}
copied[step] = trimmed
}
approvalWorkflowsMu.Lock()
defer approvalWorkflowsMu.Unlock()
approvalWorkflows[ApprovalWorkflowKey(workflowStr)] = copied
return nil
}
// WorkflowSteps returns the steps registered for the given workflow key.
func WorkflowSteps(workflow ApprovalWorkflowKey) map[ApprovalStep]string {
approvalWorkflowsMu.RLock()
defer approvalWorkflowsMu.RUnlock()
workflowStr := strings.TrimSpace(workflow.String())
if workflowStr == "" {
return nil
}
steps, ok := approvalWorkflows[ApprovalWorkflowKey(workflowStr)]
if !ok || len(steps) == 0 {
return nil
}
copied := make(map[ApprovalStep]string, len(steps))
for step, label := range steps {
copied[step] = label
}
return copied
}
// ApprovalStepName fetches the label for the target step inside the workflow.
func ApprovalStepName(workflow ApprovalWorkflowKey, step ApprovalStep) (string, bool) {
steps := WorkflowSteps(workflow)
if len(steps) == 0 {
return "", false
}
label, ok := steps[step]
return label, ok
}
// ValidateApprovalStep ensures the workflow contains the provided step.
func ValidateApprovalStep(workflow ApprovalWorkflowKey, step ApprovalStep) error {
if _, ok := ApprovalStepName(workflow, step); ok {
return nil
}
return fmt.Errorf("invalid approval step %d for workflow %s", step, workflow)
}
// IsValidApprovalAction reports whether the action is supported.
func IsValidApprovalAction(action entity.ApprovalAction) bool {
_, ok := approvalActions[action]
return ok
}
// NewApproval creates an approval record for the given approvable target.
func NewApproval(workflow ApprovalWorkflowKey, approvableId uint, step ApprovalStep, action *entity.ApprovalAction, actorId uint, note *string) (*entity.Approval, error) {
if approvableId == 0 {
return nil, errors.New("approvable id is required")
}
workflowStr := strings.TrimSpace(workflow.String())
if workflowStr == "" {
return nil, errors.New("approval workflow key is required")
}
key := ApprovalWorkflowKey(workflowStr)
if err := ValidateApprovalStep(key, step); err != nil {
return nil, err
}
var actionPtr *entity.ApprovalAction
if action != nil {
if !IsValidApprovalAction(*action) {
return nil, fmt.Errorf("invalid approval action %q", *action)
}
actionCopy := *action
actionPtr = &actionCopy
}
if actorId == 0 {
return nil, errors.New("actor id is required")
}
var notes *string
if note != nil {
trimmed := strings.TrimSpace(*note)
if trimmed != "" {
notes = &trimmed
}
}
actor := actorId
var stepName string
if label, ok := ApprovalStepName(key, step); ok {
labelCopy := label
stepName = labelCopy
}
return &entity.Approval{
ApprovableType: workflowStr,
ApprovableId: approvableId,
StepNumber: uint16(step),
StepName: stepName,
Action: actionPtr,
Notes: notes,
ActionBy: &actor,
}, nil
}
// SetApprovalAction updates the approval action, notes, and optionally advances to another step.
func SetApprovalAction(approval *entity.Approval, action entity.ApprovalAction, actorId uint, note *string, nextStep NextStepCallback) error {
if approval == nil {
return errors.New("approval is nil")
}
if !IsValidApprovalAction(action) {
return fmt.Errorf("invalid approval action %q", action)
}
if actorId == 0 {
return errors.New("actor id is required for approval decision")
}
act := action
approval.Action = &act
approval.ActionBy = &actorId
if note != nil {
trimmed := strings.TrimSpace(*note)
if trimmed == "" {
approval.Notes = nil
} else {
approval.Notes = &trimmed
}
} else {
approval.Notes = nil
}
if nextStep != nil {
current := ApprovalStep(approval.StepNumber)
if proposed, ok := nextStep(current, action); ok {
if err := ValidateApprovalStep(ApprovalWorkflowKey(approval.ApprovableType), proposed); err != nil {
return err
}
approval.StepNumber = uint16(proposed)
}
}
if label, ok := ApprovalStepName(ApprovalWorkflowKey(approval.ApprovableType), ApprovalStep(approval.StepNumber)); ok {
labelCopy := label
approval.StepName = labelCopy
}
return nil
}
// Approve marks the approval as approved by the given actor, applying the optional step callback.
func Approve(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error {
return SetApprovalAction(approval, entity.ApprovalActionApproved, actorId, note, nextStep)
}
// Reject marks the approval as rejected by the given actor, applying the optional step callback.
func Reject(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error {
return SetApprovalAction(approval, entity.ApprovalActionRejected, actorId, note, nextStep)
}
+41 -6
View File
@@ -1,6 +1,10 @@
package utils package utils
import "strings" import (
"strings"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// FlagType & Groups // FlagType & Groups
@@ -59,7 +63,6 @@ var allFlagTypes = func() map[FlagType]struct{} {
return m return m
}() }()
func AllFlagTypes() map[FlagType]struct{} { func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes return allFlagTypes
} }
@@ -76,8 +79,6 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG" WarehouseTypeKandang WarehouseType = "KANDANG"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -100,8 +101,6 @@ const (
SupplierCategorySapronak SupplierCategory = "SAPRONAK" SupplierCategorySapronak SupplierCategory = "SAPRONAK"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Kandang Status // Kandang Status
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -113,6 +112,34 @@ const (
KandangStatusPengajuan KandangStatus = "PENGAJUAN" KandangStatusPengajuan KandangStatus = "PENGAJUAN"
KandangStatusActive KandangStatus = "ACTIVE" KandangStatusActive KandangStatus = "ACTIVE"
) )
// -------------------------------------------------------------------
// ProjectFlockCategory
// -------------------------------------------------------------------
type ProjectFlockCategory string
const (
ProjectFlockCategoryGrowing ProjectFlockCategory = "GROWING"
ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING"
)
// -------------------------------------------------------------------
// Project Flock Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS")
ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1
ProjectFlockStepAktif approvalutils.ApprovalStep = 2
)
// projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals.
var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockStepPengajuan: "Pengajuan",
ProjectFlockStepAktif: "Aktif",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -223,6 +250,14 @@ func IsValidCustomerSupplierType(v string) bool {
return false return false
} }
func IsValidProjectFlockCategory(v string) bool {
switch ProjectFlockCategory(v) {
case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying:
return true
}
return false
}
func IsValidSupplierCategory(v string) bool { func IsValidSupplierCategory(v string) bool {
switch SupplierCategory(v) { switch SupplierCategory(v) {
case SupplierCategoryBOP, SupplierCategorySapronak: case SupplierCategoryBOP, SupplierCategorySapronak:
+2 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
func TestKandangIntegration(t *testing.T) { func TestKandangIntegration(t *testing.T) {
@@ -51,7 +52,6 @@ func TestKandangIntegration(t *testing.T) {
}) })
t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) { t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) {
categoryID := createProductCategory(t, app, "DOC Category", "DOC1")
fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{ fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -60,7 +60,7 @@ func TestKandangIntegration(t *testing.T) {
projectFloc := entities.ProjectFlock{ projectFloc := entities.ProjectFlock{
FlockId: flocID, FlockId: flocID,
AreaId: areaID, AreaId: areaID,
ProductCategoryId: categoryID, Category: string(utils.ProjectFlockCategoryGrowing),
FcrId: fcrID, FcrId: fcrID,
LocationId: locationID, LocationId: locationID,
Period: 1, Period: 1,
@@ -19,7 +19,6 @@ func TestProjectFlockSummary(t *testing.T) {
areaID := createArea(t, app, "Area Project") areaID := createArea(t, app, "Area Project")
locationID := createLocation(t, app, "Location Project", "Address", areaID) locationID := createLocation(t, app, "Location Project", "Address", areaID)
flockID := createFlock(t, app, "Flock Summary") flockID := createFlock(t, app, "Flock Summary")
categoryID := createProductCategory(t, app, "DOC Summary", "DOCS")
fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -28,7 +27,7 @@ func TestProjectFlockSummary(t *testing.T) {
createPayload := map[string]any{ createPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{kandangID}, "kandang_ids": []uint{kandangID},
@@ -42,6 +41,7 @@ func TestProjectFlockSummary(t *testing.T) {
Data struct { Data struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
Flock struct { Flock struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -50,11 +50,6 @@ func TestProjectFlockSummary(t *testing.T) {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} `json:"area"` } `json:"area"`
ProductCategory struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"product_category"`
Fcr struct { Fcr struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -86,19 +81,27 @@ func TestProjectFlockSummary(t *testing.T) {
if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area)
} }
if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category)
}
if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" {
t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location)
} }
if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID {
t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
} }
if createResp.Data.Kandangs[0].Status == "" { if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0]) t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
} }
if createResp.Data.Period != 1 { if createResp.Data.Period != 1 {
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
} }
createdKandang := fetchKandang(t, db, kandangID)
if createdKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status)
}
var pivotRecords []entities.ProjectFlockKandang var pivotRecords []entities.ProjectFlockKandang
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch pivot records: %v", err) t.Fatalf("failed to fetch pivot records: %v", err)
@@ -110,15 +113,12 @@ func TestProjectFlockSummary(t *testing.T) {
if firstPivotRecord.KandangId != kandangID { if firstPivotRecord.KandangId != kandangID {
t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId) t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
} }
if firstPivotRecord.DetachedAt != nil {
t.Fatalf("expected pivot DetachedAt to be nil for active assignment, got %v", firstPivotRecord.DetachedAt)
}
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
secondPayload := map[string]any{ secondPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "laying",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{secondKandangID}, "kandang_ids": []uint{secondKandangID},
@@ -131,6 +131,7 @@ func TestProjectFlockSummary(t *testing.T) {
Data struct { Data struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
} `json:"data"` } `json:"data"`
} }
if err := json.Unmarshal(body, &createRespSecond); err != nil { if err := json.Unmarshal(body, &createRespSecond); err != nil {
@@ -139,6 +140,9 @@ func TestProjectFlockSummary(t *testing.T) {
if createRespSecond.Data.Period != 2 { if createRespSecond.Data.Period != 2 {
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
} }
if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category)
}
pivotRecords = nil pivotRecords = nil
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
@@ -151,8 +155,10 @@ func TestProjectFlockSummary(t *testing.T) {
if secondPivotRecord.KandangId != secondKandangID { if secondPivotRecord.KandangId != secondKandangID {
t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId) t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
} }
if secondPivotRecord.DetachedAt != nil {
t.Fatalf("expected second pivot DetachedAt to be nil, got %v", secondPivotRecord.DetachedAt) secondKandang := fetchKandang(t, db, secondKandangID)
if secondKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status)
} }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
@@ -186,15 +192,14 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
} }
var firstPivot entities.ProjectFlockKandang var remainingFirst int64
if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil { if err := db.Model(&entities.ProjectFlockKandang{}).
t.Fatalf("failed to reload first pivot record: %v", err) Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID).
Count(&remainingFirst).Error; err != nil {
t.Fatalf("failed to count first pivot records after delete: %v", err)
} }
if firstPivot.DetachedAt == nil { if remainingFirst != 0 {
t.Fatalf("expected first pivot DetachedAt to be set after delete") t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
}
if firstPivot.ProjectFlockId != createResp.Data.Id {
t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId)
} }
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
@@ -202,7 +207,7 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
} }
secondKandang := fetchKandang(t, db, secondKandangID) secondKandang = fetchKandang(t, db, secondKandangID)
if secondKandang.ProjectFlockId != nil { if secondKandang.ProjectFlockId != nil {
t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
} }
@@ -210,15 +215,14 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
} }
var secondPivot entities.ProjectFlockKandang var remainingSecond int64
if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil { if err := db.Model(&entities.ProjectFlockKandang{}).
t.Fatalf("failed to reload second pivot record: %v", err) Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID).
Count(&remainingSecond).Error; err != nil {
t.Fatalf("failed to count second pivot records after delete: %v", err)
} }
if secondPivot.DetachedAt == nil { if remainingSecond != 0 {
t.Fatalf("expected second pivot DetachedAt to be set after delete") t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
}
if secondPivot.ProjectFlockId != createRespSecond.Data.Id {
t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId)
} }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
@@ -245,7 +249,6 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
areaID := createArea(t, app, "Area Search Target") areaID := createArea(t, app, "Area Search Target")
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
flockID := createFlock(t, app, "Flock Search Target") flockID := createFlock(t, app, "Flock Search Target")
categoryID := createProductCategory(t, app, "Category Search Target", "CATGT")
fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -254,7 +257,7 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
createPayload := map[string]any{ createPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{kandangID}, "kandang_ids": []uint{kandangID},
@@ -277,8 +280,8 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
searchTerms := []string{ searchTerms := []string{
"Flock Search Target", "Flock Search Target",
"Area Search Target", "Area Search Target",
"Category Search Target", string(utils.ProjectFlockCategoryGrowing),
"CATGT", "growing",
"FCR Search Target", "FCR Search Target",
"Kandang Search Target", "Kandang Search Target",
"Location Search Target", "Location Search Target",
@@ -329,7 +332,6 @@ func TestProjectFlockSorting(t *testing.T) {
flockOne := createFlock(t, app, "Flock Sort One") flockOne := createFlock(t, app, "Flock Sort One")
flockTwo := createFlock(t, app, "Flock Sort Two") flockTwo := createFlock(t, app, "Flock Sort Two")
categoryID := createProductCategory(t, app, "Category Sort", "CSORT")
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -341,7 +343,7 @@ func TestProjectFlockSorting(t *testing.T) {
projectOnePayload := map[string]any{ projectOnePayload := map[string]any{
"flock_id": flockOne, "flock_id": flockOne,
"area_id": areaA, "area_id": areaA,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationA, "location_id": locationA,
"kandang_ids": []uint{kandangOne}, "kandang_ids": []uint{kandangOne},
@@ -355,7 +357,7 @@ func TestProjectFlockSorting(t *testing.T) {
projectTwoPayload := map[string]any{ projectTwoPayload := map[string]any{
"flock_id": flockTwo, "flock_id": flockTwo,
"area_id": areaB, "area_id": areaB,
"product_category_id": categoryID, "category": "laying",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationB, "location_id": locationB,
"kandang_ids": []uint{kandangTwo, kandangThree}, "kandang_ids": []uint{kandangTwo, kandangThree},
+2 -2
View File
@@ -9,8 +9,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
} }
{{end}} {{end}}