diff --git a/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.down.sql b/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.down.sql new file mode 100644 index 00000000..48e4ff1a --- /dev/null +++ b/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.down.sql @@ -0,0 +1,14 @@ +-- Reverse UPSERT: hapus baris PFK 47 & 48 yang kemungkinan baru diinsert oleh up migration ini. +-- Jika sebelumnya sudah ada (ON CONFLICT DO UPDATE), baris ini akan terhapus — +-- restore manual dari backup jika diperlukan. +DELETE FROM farm_depreciation_manual_inputs +WHERE project_flock_id IN (47, 48); + +-- UPDATE rows untuk PFK 4–27 tidak bisa di-reverse secara presisi: +-- nilai total_cost sebelum migration ini tidak tersimpan di migration history +-- (data awal di-load via cmd/import-farm-depreciation-manual-inputs dari Excel). +-- PFK 10 dan 11 tidak berubah (nilai sama dengan state dari migration 20260529144559). +-- Jika perlu rollback penuh: restore dari database backup atau re-import Excel lama. + +-- Recompute snapshots setelah rollback +TRUNCATE TABLE farm_depreciation_snapshots; diff --git a/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.up.sql b/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.up.sql new file mode 100644 index 00000000..24a72a2e --- /dev/null +++ b/internal/database/migrations/20260607145605_update_farm_depreciation_manual_inputs.up.sql @@ -0,0 +1,105 @@ + + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 1900157533.55, + cutover_date = DATE '2026-02-28', + updated_at = NOW() +WHERE project_flock_id = 10; + + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 146658321.066, + cutover_date = DATE '2026-02-28', + updated_at = NOW() +WHERE project_flock_id = 13; + + + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 51824694.138, + cutover_date = DATE '2026-02-28', + updated_at = NOW() +WHERE project_flock_id = 17; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 15491774.796, + cutover_date = DATE '2026-02-28', + updated_at = NOW() +WHERE project_flock_id = 8; + + + + +-- Cutover 2026-02-28 (lanjutan) +UPDATE farm_depreciation_manual_inputs +SET total_cost = 575074391.36, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 4; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 578360642.51, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 5; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 880983605.92, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 6; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 391669576.153, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 9; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 2521797832.14, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 11; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 139227054.164, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 12; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 380083106.836, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 14; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 705136853.847, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 15; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 209816474.000, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 18; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 557606867.000, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 19; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 239330456.11, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 20; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 4724203916.72, cutover_date = DATE '2026-02-28', updated_at = NOW() +WHERE project_flock_id = 26; + +-- Cutover 2026-05-15 +UPDATE farm_depreciation_manual_inputs +SET total_cost = 5449963647.43, cutover_date = DATE '2026-05-15', updated_at = NOW() +WHERE project_flock_id = 27; + +-- Cutover 2026-06-08 (upsert — row mungkin belum ada) +INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at) +VALUES (47, 5395429899.42, DATE '2026-06-08', NOW(), NOW()) +ON CONFLICT (project_flock_id) DO UPDATE + SET total_cost = EXCLUDED.total_cost, + cutover_date = EXCLUDED.cutover_date, + updated_at = NOW(); + +-- Cutover 2026-06-16 (upsert — row mungkin belum ada) +INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at) +VALUES (48, 5514616442.08, DATE '2026-06-16', NOW(), NOW()) +ON CONFLICT (project_flock_id) DO UPDATE + SET total_cost = EXCLUDED.total_cost, + cutover_date = EXCLUDED.cutover_date, + updated_at = NOW(); + +-- Pengaman: pastikan snapshot di-recompute dengan total_cost baru +-- saat user request /api/reports/expense/depreciation +TRUNCATE TABLE farm_depreciation_snapshots; diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 85c19f6e..46083db7 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -34,7 +34,6 @@ import ( "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" - "gorm.io/gorm/clause" ) type RecordingService interface { @@ -586,10 +585,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent s.Log.Errorf("Failed to recalculate recordings after create: %+v", err) return err } - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err) - return err - } action := entity.ApprovalActionCreated if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil { @@ -892,12 +887,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } } - if hasStockChanges { - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err) - return err - } - } action := entity.ApprovalActionUpdated actorID := recordingEntity.CreatedBy @@ -1159,10 +1148,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err) return err } - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err) - return err - } s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime) return nil @@ -1949,172 +1934,6 @@ func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx con return row.ChickInDate, nil } -func (s *recordingService) syncFarmDepreciationManualInputFromRecordingStocks( - ctx context.Context, - tx *gorm.DB, - projectFlockKandangID uint, - fallbackCutoverDate time.Time, -) error { - if projectFlockKandangID == 0 { - return nil - } - - targetDB := s.Repository.DB() - if tx != nil { - targetDB = tx - } - - projectFlockID, err := s.resolveProjectFlockIDByProjectFlockKandangID(ctx, targetDB, projectFlockKandangID) - if err != nil { - return err - } - if projectFlockID == 0 { - return nil - } - - totalCost, err := s.sumNoTransferRecordingStockCostByProjectFlockID(ctx, targetDB, projectFlockID) - if err != nil { - return err - } - - existing, err := s.getFarmDepreciationManualInputByProjectFlockID(ctx, targetDB, projectFlockID) - if err != nil { - return err - } - - cutoverDate := normalizeDateOnlyUTC(fallbackCutoverDate) - if existing != nil && !existing.CutoverDate.IsZero() { - cutoverDate = normalizeDateOnlyUTC(existing.CutoverDate) - } - if cutoverDate.IsZero() { - earliestDate, dateErr := s.getEarliestNoTransferRecordingDateByProjectFlockID(ctx, targetDB, projectFlockID) - if dateErr != nil { - return dateErr - } - if earliestDate != nil && !earliestDate.IsZero() { - cutoverDate = normalizeDateOnlyUTC(*earliestDate) - } - } - if cutoverDate.IsZero() { - cutoverDate = normalizeDateOnlyUTC(time.Now().UTC()) - } - - now := time.Now().UTC() - row := entity.FarmDepreciationManualInput{ - ProjectFlockId: projectFlockID, - TotalCost: totalCost, - CutoverDate: cutoverDate, - } - if existing != nil { - row.Note = existing.Note - } - - return targetDB.WithContext(ctx). - Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "project_flock_id"}}, - DoUpdates: clause.Assignments(map[string]any{ - "total_cost": row.TotalCost, - "cutover_date": row.CutoverDate, - "updated_at": now, - }), - }). - Create(&row).Error -} - -func (s *recordingService) resolveProjectFlockIDByProjectFlockKandangID(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (uint, error) { - var row struct { - ProjectFlockID uint `gorm:"column:project_flock_id"` - } - err := db.WithContext(ctx). - Table("project_flock_kandangs"). - Select("project_flock_id"). - Where("id = ?", projectFlockKandangID). - Take(&row).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil - } - if err != nil { - return 0, err - } - return row.ProjectFlockID, nil -} - -func (s *recordingService) sumNoTransferRecordingStockCostByProjectFlockID(ctx context.Context, db *gorm.DB, projectFlockID uint) (float64, error) { - if projectFlockID == 0 { - return 0, nil - } - - var total float64 - err := db.WithContext(ctx). - Table("recording_stocks AS rs"). - Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). - Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL"). - Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). - Joins( - "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", - fifo.UsableKeyRecordingStock.String(), - fifo.StockableKeyPurchaseItems.String(), - entity.StockAllocationStatusActive, - entity.StockAllocationPurposeConsume, - ). - Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). - Where("pfk.project_flock_id = ?", projectFlockID). - Where("rs.project_flock_kandang_id IS NULL"). - Scan(&total).Error - if err != nil { - return 0, err - } - return total, nil -} - -func (s *recordingService) getFarmDepreciationManualInputByProjectFlockID( - ctx context.Context, - db *gorm.DB, - projectFlockID uint, -) (*entity.FarmDepreciationManualInput, error) { - if projectFlockID == 0 { - return nil, nil - } - - var row entity.FarmDepreciationManualInput - err := db.WithContext(ctx). - Where("project_flock_id = ?", projectFlockID). - Take(&row).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - if err != nil { - return nil, err - } - return &row, nil -} - -func (s *recordingService) getEarliestNoTransferRecordingDateByProjectFlockID( - ctx context.Context, - db *gorm.DB, - projectFlockID uint, -) (*time.Time, error) { - if projectFlockID == 0 { - return nil, nil - } - - var row struct { - RecordDate *time.Time `gorm:"column:record_date"` - } - err := db.WithContext(ctx). - Table("recording_stocks AS rs"). - Select("MIN(r.record_datetime) AS record_date"). - Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL"). - Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). - Where("pfk.project_flock_id = ?", projectFlockID). - Where("rs.project_flock_kandang_id IS NULL"). - Scan(&row).Error - if err != nil { - return nil, err - } - return row.RecordDate, nil -} - func (s *recordingService) resolveEggRequestsToFarmWarehouses( ctx context.Context, pfk *entity.ProjectFlockKandang,