From 9b2b62429ca5707e9c8a9e085021c6d9c48d432b Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 20 Oct 2025 22:49:30 +0700 Subject: [PATCH] FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto seperti warehouse di projectflock --- ...tFlock_category_and_period_unique.down.sql | 25 +++++++++++ ...ectFlock_category_and_period_unique.up.sql | 43 +++++++++++++++++++ ...0_add_project_flock_period_unique.down.sql | 1 - ...000_add_project_flock_period_unique.up.sql | 3 -- internal/database/seed/seeder.go | 18 ++++---- internal/entities/projectflock_kandang.go | 9 +--- .../projectflock_kandang.repository.go | 13 +++--- .../services/projectflock.service.go | 10 ++--- .../master_data/project_flock_test.go | 36 ++++++---------- 9 files changed, 101 insertions(+), 57 deletions(-) create mode 100644 internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.down.sql create mode 100644 internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.up.sql delete mode 100644 internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql delete mode 100644 internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql diff --git a/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.down.sql b/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.down.sql new file mode 100644 index 00000000..81c50f3f --- /dev/null +++ b/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.down.sql @@ -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; diff --git a/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.up.sql b/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.up.sql new file mode 100644 index 00000000..2341a4cd --- /dev/null +++ b/internal/database/migrations/20251020154311_adjustment_projectFlock_category_and_period_unique.up.sql @@ -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; diff --git a/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql b/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql deleted file mode 100644 index f3cb3ddf..00000000 --- a/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS project_flocks_flock_period_unique; diff --git a/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql b/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql deleted file mode 100644 index 40cebe2d..00000000 --- a/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE UNIQUE INDEX project_flocks_flock_period_unique -ON project_flocks (flock_id, period) -WHERE deleted_at IS NULL; diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index e6f77b15..f718cde9 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -374,7 +374,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users if err := tx.Create(&kandang).Error; err != nil { 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 } } else if err != nil { @@ -393,7 +393,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 { 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 } } @@ -403,25 +403,24 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users 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 { return err } if projectFlockID == nil { return nil } - return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy) + return ensureActivePivot(tx, *projectFlockID, kandangID) } func detachActivePivot(tx *gorm.DB, kandangID uint) error { - return tx.Model(&entity.ProjectFlockKandang{}). - Where("kandang_id = ? AND detached_at IS NULL", kandangID). - Updates(map[string]any{"detached_at": time.Now()}).Error + return tx.Where("kandang_id = ?", kandangID). + Delete(&entity.ProjectFlockKandang{}).Error } -func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error { +func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error { 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 if err == nil { return nil @@ -432,7 +431,6 @@ func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) e newRecord := entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: kandangID, - CreatedBy: createdBy, } return tx.Create(&newRecord).Error } diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index 0014a815..1c29c22e 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -4,14 +4,9 @@ import "time" type ProjectFlockKandang struct { 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"` - KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_active,priority:2,where:detached_at IS NULL"` - CreatedBy uint `gorm:"not null"` - AssignedAt time.Time `gorm:"autoCreateTime"` - DetachedAt *time.Time `gorm:"index"` + 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_unique"` CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 9b89a399..b5ce21a4 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -2,7 +2,6 @@ package repository import ( "context" - "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -10,7 +9,7 @@ import ( type ProjectFlockKandangRepository interface { 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) WithTx(tx *gorm.DB) ProjectFlockKandangRepository DB() *gorm.DB @@ -31,14 +30,13 @@ func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, reco 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 { return nil } return r.db.WithContext(ctx). - Model(&entity.ProjectFlockKandang{}). - Where("project_flock_id = ? AND kandang_id IN ? AND detached_at IS NULL", projectFlockID, kandangIDs). - Updates(map[string]any{"detached_at": detachedAt}).Error + Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs). + Delete(&entity.ProjectFlockKandang{}).Error } func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) { @@ -47,8 +45,7 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit Preload("ProjectFlock"). Preload("ProjectFlock.Flock"). Preload("Kandang"). - Preload("CreatedUser"). - Order("project_flock_id ASC, assigned_at ASC"). + Order("project_flock_id ASC, created_at ASC"). Find(&records).Error; err != nil { return nil, err } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 18f00b7d..21941826 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strings" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" common "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -242,7 +241,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } - if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs, createBody.CreatedBy); err != nil { + if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs); err != nil { tx.Rollback() s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err) return nil, err @@ -395,7 +394,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if len(toAttach) > 0 { - if err := s.attachKandangs(c.Context(), tx, id, toAttach, existing.CreatedBy); err != nil { + if err := s.attachKandangs(c.Context(), tx, id, toAttach); err != nil { tx.Rollback() s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err) return nil, err @@ -533,7 +532,7 @@ 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, tx *gorm.DB, projectFlockID uint, kandangIDs []uint) error { if len(kandangIDs) == 0 { return nil } @@ -553,7 +552,6 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr records[i] = &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, - CreatedBy: createdBy, } } if err := pivotRepo.CreateMany(ctx, records); err != nil { @@ -578,7 +576,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, pr 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(tx).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } return nil diff --git a/test/integration/master_data/project_flock_test.go b/test/integration/master_data/project_flock_test.go index fbeb804c..60bb2d90 100644 --- a/test/integration/master_data/project_flock_test.go +++ b/test/integration/master_data/project_flock_test.go @@ -113,9 +113,6 @@ func TestProjectFlockSummary(t *testing.T) { if firstPivotRecord.KandangId != 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) secondPayload := map[string]any{ @@ -158,9 +155,6 @@ func TestProjectFlockSummary(t *testing.T) { if secondPivotRecord.KandangId != secondKandangID { 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) { @@ -198,15 +192,14 @@ func TestProjectFlockSummary(t *testing.T) { t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) } - var firstPivot entities.ProjectFlockKandang - if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil { - t.Fatalf("failed to reload first pivot record: %v", err) + var remainingFirst int64 + if err := db.Model(&entities.ProjectFlockKandang{}). + 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 { - t.Fatalf("expected first pivot DetachedAt to be set after delete") - } - if firstPivot.ProjectFlockId != createResp.Data.Id { - t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId) + if remainingFirst != 0 { + t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst) } resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) @@ -222,15 +215,14 @@ func TestProjectFlockSummary(t *testing.T) { t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) } - var secondPivot entities.ProjectFlockKandang - if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil { - t.Fatalf("failed to reload second pivot record: %v", err) + var remainingSecond int64 + if err := db.Model(&entities.ProjectFlockKandang{}). + 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 { - t.Fatalf("expected second pivot DetachedAt to be set after delete") - } - if secondPivot.ProjectFlockId != createRespSecond.Data.Id { - t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId) + if remainingSecond != 0 { + t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond) } resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)