From f51fa0a16c39fcea989c1b471d9cee228b9ca18b Mon Sep 17 00:00:00 2001 From: giovanni Date: Wed, 22 Apr 2026 12:57:41 +0700 Subject: [PATCH] adjust repo hpp v2 --- .../repository/common.hppv2.repository.go | 34 +- .../common.hppv2.repository_test.go | 113 ++++++- .../repport.expense_depreciation_test.go | 293 +++++++++++++++++- .../repports/services/repport.service.go | 57 ++-- .../validations/repport.validation.go | 1 + 5 files changed, 458 insertions(+), 40 deletions(-) diff --git a/internal/common/repository/common.hppv2.repository.go b/internal/common/repository/common.hppv2.repository.go index 81750bdd..0968ad0e 100644 --- a/internal/common/repository/common.hppv2.repository.go +++ b/internal/common/repository/common.hppv2.repository.go @@ -266,6 +266,26 @@ func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlo utils.FlagVitamin, utils.FlagKimia, } + transferExistsCondition := ` + EXISTS ( + SELECT 1 + FROM laying_transfer_targets AS ltt + JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id + WHERE ltt.deleted_at IS NULL + AND lt.deleted_at IS NULL + AND lt.executed_at IS NOT NULL + AND ltt.target_project_flock_kandang_id = r.project_flock_kandangs_id + AND COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) <= DATE(?) + AND ( + SELECT a.action + FROM approvals a + WHERE a.approvable_type = ? + AND a.approvable_id = lt.id + ORDER BY a.id DESC + LIMIT 1 + ) = ? + ) + ` var total float64 err := r.db.WithContext(ctx). @@ -284,7 +304,19 @@ func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlo Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). Where("pfk_rec.project_flock_id = ?", projectFlockID). Where("DATE(r.record_datetime) <= DATE(?)", periodDate). - Where("(rs.project_flock_kandang_id IS NULL OR rs.project_flock_kandang_id <> r.project_flock_kandangs_id)"). + Where( + fmt.Sprintf( + "((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)", + transferExistsCondition, + transferExistsCondition, + ), + periodDate, + string(utils.ApprovalWorkflowTransferToLaying), + entity.ApprovalActionApproved, + periodDate, + string(utils.ApprovalWorkflowTransferToLaying), + entity.ApprovalActionApproved, + ). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags). Scan(&total).Error if err != nil { diff --git a/internal/common/repository/common.hppv2.repository_test.go b/internal/common/repository/common.hppv2.repository_test.go index 5e60f8c2..1e76b2d5 100644 --- a/internal/common/repository/common.hppv2.repository_test.go +++ b/internal/common/repository/common.hppv2.repository_test.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "math" "testing" "time" @@ -98,33 +99,95 @@ func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments( func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) { db := setupHppV2RepositoryTestDB(t) + approvalType := utils.ApprovalWorkflowTransferToLaying.String() mustExecHppV2(t, db, - `INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES (101, 1, 1), (201, 2, 2)`, + `INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES + (101, 1, 1), + (102, 2, 1), + (103, 3, 1), + (104, 4, 1), + (105, 5, 1), + (201, 6, 2)`, `INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES (1, 101, '2026-04-10 08:00:00', NULL), - (2, 101, '2026-04-11 08:00:00', NULL), - (3, 101, '2026-04-12 08:00:00', NULL)`, + (2, 101, '2026-04-10 08:05:00', NULL), + (3, 101, '2026-04-10 08:10:00', NULL), + (4, 102, '2026-04-10 08:15:00', NULL), + (5, 102, '2026-04-10 08:20:00', NULL), + (6, 103, '2026-04-12 08:00:00', NULL), + (7, 103, '2026-04-12 08:05:00', NULL), + (8, 104, '2026-04-12 08:10:00', NULL), + (9, 104, '2026-04-12 08:15:00', NULL), + (10, 105, '2026-04-12 08:20:00', NULL), + (11, 105, '2026-04-12 08:25:00', NULL)`, `INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES (501, 201, 10, NULL), - (502, 201, 11, NULL), - (503, 201, 12, NULL)`, + (502, 201, 10, NULL), + (503, 201, 10, NULL), + (504, 201, 10, NULL), + (505, 201, 10, NULL), + (506, 201, 10, NULL), + (507, 201, 10, NULL), + (508, 201, 10, NULL), + (509, 201, 10, NULL), + (510, 201, 10, NULL), + (511, 201, 10, NULL)`, `INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES - (10, 'products', 10, 'PAKAN'), - (11, 'products', 11, 'OVK'), - (12, 'products', 12, 'PAKAN')`, + (10, 'products', 10, 'PAKAN')`, `INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, project_flock_kandang_id) VALUES (101, 1, 501, NULL), (102, 2, 502, 201), - (103, 3, 503, 101)`, + (103, 3, 503, 101), + (104, 4, 504, NULL), + (105, 5, 505, 201), + (106, 6, 506, NULL), + (107, 7, 507, 201), + (108, 8, 508, NULL), + (109, 9, 509, 201), + (110, 10, 510, NULL), + (111, 11, 511, 201)`, `INSERT INTO purchase_items (id, product_id, price) VALUES (601, 10, 100), - (602, 11, 200), - (603, 12, 300)`, + (602, 10, 110), + (603, 10, 120), + (604, 10, 130), + (605, 10, 140), + (606, 10, 150), + (607, 10, 160), + (608, 10, 170), + (609, 10, 180), + (610, 10, 190), + (611, 10, 200)`, `INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose, qty) VALUES (9001, 'RECORDING_STOCK', 101, 'PURCHASE_ITEMS', 601, 'ACTIVE', 'CONSUME', 2), - (9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1.5), - (9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1)`, + (9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1), + (9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1), + (9004, 'RECORDING_STOCK', 104, 'PURCHASE_ITEMS', 604, 'ACTIVE', 'CONSUME', 1), + (9005, 'RECORDING_STOCK', 105, 'PURCHASE_ITEMS', 605, 'ACTIVE', 'CONSUME', 1), + (9006, 'RECORDING_STOCK', 106, 'PURCHASE_ITEMS', 606, 'ACTIVE', 'CONSUME', 1), + (9007, 'RECORDING_STOCK', 107, 'PURCHASE_ITEMS', 607, 'ACTIVE', 'CONSUME', 1), + (9008, 'RECORDING_STOCK', 108, 'PURCHASE_ITEMS', 608, 'ACTIVE', 'CONSUME', 1), + (9009, 'RECORDING_STOCK', 109, 'PURCHASE_ITEMS', 609, 'ACTIVE', 'CONSUME', 1), + (9010, 'RECORDING_STOCK', 110, 'PURCHASE_ITEMS', 610, 'ACTIVE', 'CONSUME', 1), + (9011, 'RECORDING_STOCK', 111, 'PURCHASE_ITEMS', 611, 'ACTIVE', 'CONSUME', 1)`, + `INSERT INTO laying_transfers (id, transfer_date, effective_move_date, economic_cutoff_date, executed_at, deleted_at) VALUES + (1001, '2026-04-04', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL), + (1002, '2026-05-01', '2026-05-01', NULL, '2026-05-01 00:00:00', NULL), + (1003, '2026-04-03', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL), + (1004, '2026-04-03', '2026-04-05', NULL, NULL, NULL)`, + `INSERT INTO laying_transfer_targets (id, laying_transfer_id, target_project_flock_kandang_id, deleted_at) VALUES + (2001, 1001, 101, NULL), + (2002, 1002, 103, NULL), + (2003, 1003, 104, NULL), + (2004, 1004, 105, NULL)`, + fmt.Sprintf(`INSERT INTO approvals (id, approvable_type, approvable_id, action) VALUES + (3001, '%s', 1001, 'APPROVED'), + (3002, '%s', 1002, 'APPROVED'), + (3003, '%s', 1003, 'APPROVED'), + (3004, '%s', 1003, 'REJECTED'), + (3005, '%s', 1004, 'APPROVED')`, + approvalType, approvalType, approvalType, approvalType, approvalType), ) repo := &HppV2RepositoryImpl{db: db} @@ -134,14 +197,14 @@ func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t if err != nil { t.Fatalf("expected no error, got %v", err) } - assertFloatEquals(t, total, 500) + assertFloatEquals(t, total, 750) earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59") earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod) if err != nil { t.Fatalf("expected no error, got %v", err) } - assertFloatEquals(t, earlyTotal, 200) + assertFloatEquals(t, earlyTotal, 240) } func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB { @@ -246,6 +309,26 @@ func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB { flagable_id INTEGER NULL, name TEXT NULL )`, + `CREATE TABLE laying_transfers ( + id INTEGER PRIMARY KEY, + transfer_date DATETIME NULL, + effective_move_date DATETIME NULL, + economic_cutoff_date DATETIME NULL, + executed_at DATETIME NULL, + deleted_at DATETIME NULL + )`, + `CREATE TABLE laying_transfer_targets ( + id INTEGER PRIMARY KEY, + laying_transfer_id INTEGER NULL, + target_project_flock_kandang_id INTEGER NULL, + deleted_at DATETIME NULL + )`, + `CREATE TABLE approvals ( + id INTEGER PRIMARY KEY, + approvable_type TEXT NULL, + approvable_id INTEGER NULL, + action TEXT NULL + )`, ) return db diff --git a/internal/modules/repports/services/repport.expense_depreciation_test.go b/internal/modules/repports/services/repport.expense_depreciation_test.go index 820fbaa6..3f10e428 100644 --- a/internal/modules/repports/services/repport.expense_depreciation_test.go +++ b/internal/modules/repports/services/repport.expense_depreciation_test.go @@ -13,6 +13,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -21,12 +22,17 @@ import ( type expenseDepreciationRepoMock struct { repportRepo.ExpenseDepreciationRepository - manualInputs []repportRepo.FarmDepreciationManualInputRow + manualInputs []repportRepo.FarmDepreciationManualInputRow + candidateRows []repportRepo.FarmDepreciationCandidateRow + snapshots []entity.FarmDepreciationSnapshot - upsertedRow *entity.FarmDepreciationManualInput - deleteCalled bool - deleteDate time.Time - deleteFarmIDs []uint + upsertedRow *entity.FarmDepreciationManualInput + deleteCalled bool + deleteDate time.Time + deleteFarmIDs []uint + upsertSnapshotCalls int + upsertedSnapshots []entity.FarmDepreciationSnapshot + getSnapshotsCalls int } func (m *expenseDepreciationRepoMock) DB() *gorm.DB { @@ -46,6 +52,37 @@ func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row * return nil } +func (m *expenseDepreciationRepoMock) GetCandidateFarms(_ context.Context, _ time.Time, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationCandidateRow, error) { + return append([]repportRepo.FarmDepreciationCandidateRow{}, m.candidateRows...), nil +} + +func (m *expenseDepreciationRepoMock) GetSnapshotsByPeriodAndFarmIDs(_ context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) { + m.getSnapshotsCalls++ + if len(farmIDs) == 0 { + return []entity.FarmDepreciationSnapshot{}, nil + } + allowed := make(map[uint]struct{}, len(farmIDs)) + for _, farmID := range farmIDs { + allowed[farmID] = struct{}{} + } + result := make([]entity.FarmDepreciationSnapshot, 0, len(m.snapshots)) + for _, row := range m.snapshots { + if _, ok := allowed[row.ProjectFlockId]; !ok { + continue + } + if row.PeriodDate.IsZero() || row.PeriodDate.Format("2006-01-02") == period.Format("2006-01-02") { + result = append(result, row) + } + } + return result, nil +} + +func (m *expenseDepreciationRepoMock) UpsertSnapshots(_ context.Context, rows []entity.FarmDepreciationSnapshot) error { + m.upsertSnapshotCalls++ + m.upsertedSnapshots = append([]entity.FarmDepreciationSnapshot{}, rows...) + return nil +} + func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error { m.deleteCalled = true m.deleteDate = fromDate @@ -57,6 +94,15 @@ func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Con return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil } +type expenseRealizationRepoMock struct { + expenseRepo.ExpenseRealizationRepository + db *gorm.DB +} + +func (m *expenseRealizationRepoMock) DB() *gorm.DB { + return m.db +} + type hppCostRepoMock struct { commonRepo.HppCostRepository kandangIDsByFarm map[uint][]uint @@ -352,6 +398,167 @@ func TestComputeExpenseDepreciationSnapshots_ZeroWhenDepreciationMissing(t *test } } +func TestGetExpenseDepreciation_UsesExistingSnapshotWhenForceRecomputeFalse(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + repo := &expenseDepreciationRepoMock{ + candidateRows: []repportRepo.FarmDepreciationCandidateRow{ + {ProjectFlockID: 1, FarmName: "Farm A"}, + }, + snapshots: []entity.FarmDepreciationSnapshot{ + { + ProjectFlockId: 1, + PeriodDate: periodDate, + DepreciationPercentEffective: 11.1, + DepreciationValue: 111, + PulletCostDayNTotal: 1001, + Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`), + }, + }, + } + + svc := &repportService{ + Validate: validator.New(), + ExpenseDepreciationRepo: repo, + ExpenseRealizationRepo: &expenseRealizationRepoMock{}, + HppCostRepo: &hppCostRepoMock{}, + HppV2Svc: &hppV2ServiceMock{}, + } + + rows, meta, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0].DepreciationValue != 111 { + t.Fatalf("expected depreciation value 111, got %v", rows[0].DepreciationValue) + } + if meta == nil || meta.TotalResults != 1 { + t.Fatalf("expected meta total_results 1, got %+v", meta) + } + if repo.upsertSnapshotCalls != 0 { + t.Fatalf("expected no snapshot upsert, got %d", repo.upsertSnapshotCalls) + } + if repo.getSnapshotsCalls != 1 { + t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls) + } +} + +func TestGetExpenseDepreciation_ForceRecomputeRebuildsAllSnapshots(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + repo := &expenseDepreciationRepoMock{ + candidateRows: []repportRepo.FarmDepreciationCandidateRow{ + {ProjectFlockID: 1, FarmName: "Farm A"}, + }, + snapshots: []entity.FarmDepreciationSnapshot{ + { + ProjectFlockId: 1, + PeriodDate: periodDate, + DepreciationValue: 999, + PulletCostDayNTotal: 999, + }, + }, + } + + svc := &repportService{ + Validate: validator.New(), + ExpenseDepreciationRepo: repo, + ExpenseRealizationRepo: &expenseRealizationRepoMock{}, + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 1: {10}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 10: depreciationBreakdown(10, 100, "Kandang A", 100, 1000, 10), + }, + }, + } + + rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05&force_recompute=true") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0].DepreciationValue != 100 { + t.Fatalf("expected recomputed depreciation value 100, got %v", rows[0].DepreciationValue) + } + if repo.upsertSnapshotCalls != 1 { + t.Fatalf("expected snapshot upsert called once, got %d", repo.upsertSnapshotCalls) + } + if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 1 { + t.Fatalf("expected upserted snapshot for farm 1, got %+v", repo.upsertedSnapshots) + } + if repo.getSnapshotsCalls != 0 { + t.Fatalf("expected no snapshot fetch in force recompute mode, got %d", repo.getSnapshotsCalls) + } +} + +func TestGetExpenseDepreciation_ForceRecomputeFalseComputesOnlyMissingFarms(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + repo := &expenseDepreciationRepoMock{ + candidateRows: []repportRepo.FarmDepreciationCandidateRow{ + {ProjectFlockID: 1, FarmName: "Farm A"}, + {ProjectFlockID: 2, FarmName: "Farm B"}, + }, + snapshots: []entity.FarmDepreciationSnapshot{ + { + ProjectFlockId: 1, + PeriodDate: periodDate, + DepreciationPercentEffective: 11.1, + DepreciationValue: 111, + PulletCostDayNTotal: 1001, + Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`), + }, + }, + } + + svc := &repportService{ + Validate: validator.New(), + ExpenseDepreciationRepo: repo, + ExpenseRealizationRepo: &expenseRealizationRepoMock{}, + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 1: {10}, + 2: {20}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 10: depreciationBreakdown(10, 100, "Kandang A", 999, 9999, 10), + 20: depreciationBreakdown(20, 200, "Kandang B", 200, 2000, 10), + }, + }, + } + + rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 2 { + t.Fatalf("expected 2 rows, got %d", len(rows)) + } + if rows[0].ProjectFlockID != 1 || rows[0].DepreciationValue != 111 { + t.Fatalf("expected farm 1 use existing snapshot value 111, got %+v", rows[0]) + } + if rows[1].ProjectFlockID != 2 || rows[1].DepreciationValue != 200 { + t.Fatalf("expected farm 2 recomputed value 200, got %+v", rows[1]) + } + if repo.upsertSnapshotCalls != 1 { + t.Fatalf("expected one upsert call for missing farms, got %d", repo.upsertSnapshotCalls) + } + if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 2 { + t.Fatalf("expected upsert only farm 2 snapshot, got %+v", repo.upsertedSnapshots) + } + if repo.getSnapshotsCalls != 1 { + t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls) + } +} + func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) { repo := &expenseDepreciationRepoMock{ manualInputs: []repportRepo.FarmDepreciationManualInputRow{ @@ -411,6 +618,82 @@ func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDat } } +func getExpenseDepreciationByQuery(t *testing.T, svc *repportService, query string) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { + t.Helper() + + app := fiber.New() + var ( + rows []dto.ExpenseDepreciationRowDTO + meta *dto.ExpenseDepreciationMetaDTO + ) + app.Get("/", func(c *fiber.Ctx) error { + resultRows, resultMeta, err := svc.GetExpenseDepreciation(c) + if err != nil { + return err + } + rows = resultRows + meta = resultMeta + return c.SendStatus(fiber.StatusOK) + }) + + target := "/" + if query != "" { + target += "?" + query + } + resp, err := app.Test(httptest.NewRequest(http.MethodGet, target, nil)) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != fiber.StatusOK { + return nil, nil, fiber.NewError(resp.StatusCode, "request failed") + } + return rows, meta, nil +} + +func depreciationBreakdown( + projectFlockKandangID uint, + kandangID uint, + kandangName string, + depreciationValue float64, + pulletCostDayN float64, + depreciationPercent float64, +) *approvalService.HppV2Breakdown { + return &approvalService.HppV2Breakdown{ + ProjectFlockKandangID: projectFlockKandangID, + KandangID: kandangID, + KandangName: kandangName, + HouseType: "close_house", + Components: []approvalService.HppV2Component{ + { + Code: "DEPRECIATION", + Title: "Depreciation", + Total: depreciationValue, + Parts: []approvalService.HppV2ComponentPart{ + { + Code: "normal_transfer", + Total: depreciationValue, + Details: map[string]any{ + "schedule_day": 1, + "depreciation_percent": depreciationPercent, + "pullet_cost_day_n": pulletCostDayN, + "source_project_flock_id": 77, + "origin_date": "2026-01-01", + }, + References: []approvalService.HppV2Reference{ + { + Type: "laying_transfer", + ID: 701, + Date: "2026-05-20", + Qty: 100, + }, + }, + }, + }, + }, + }, + } +} + func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents { t.Helper() var out depreciationFarmComponents diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c5e89e3c..02bfdf5a 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -231,25 +231,9 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe farmNameByID[row.ProjectFlockID] = row.FarmName } - snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs) - if err != nil { - return nil, nil, err - } - snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots)) - for _, row := range snapshots { - snapshotByFarmID[row.ProjectFlockId] = row - } - - missingFarmIDs := make([]uint, 0) - for _, farmID := range farmIDs { - if _, exists := snapshotByFarmID[farmID]; exists { - continue - } - missingFarmIDs = append(missingFarmIDs, farmID) - } - - if len(missingFarmIDs) > 0 { - computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID) + snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot) + if params.ForceRecompute { + computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID) if computeErr != nil { return nil, nil, computeErr } @@ -257,10 +241,43 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil { return nil, nil, err } + snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots)) for _, row := range computedSnapshots { snapshotByFarmID[row.ProjectFlockId] = row } } + } else { + snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs) + if err != nil { + return nil, nil, err + } + snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots)) + for _, row := range snapshots { + snapshotByFarmID[row.ProjectFlockId] = row + } + + missingFarmIDs := make([]uint, 0) + for _, farmID := range farmIDs { + if _, exists := snapshotByFarmID[farmID]; exists { + continue + } + missingFarmIDs = append(missingFarmIDs, farmID) + } + + if len(missingFarmIDs) > 0 { + computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID) + if computeErr != nil { + return nil, nil, computeErr + } + if len(computedSnapshots) > 0 { + if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil { + return nil, nil, err + } + for _, row := range computedSnapshots { + snapshotByFarmID[row.ProjectFlockId] = row + } + } + } } rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows)) @@ -2717,6 +2734,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat rawLocation := ctx.Query("location_id", "") rawProjectFlock := ctx.Query("project_flock_id", "") period := strings.TrimSpace(ctx.Query("period", "")) + forceRecompute := ctx.QueryBool("force_recompute", false) areaIDs, err := parseCommaSeparatedInt64s(rawArea) if err != nil { @@ -2766,6 +2784,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat Page: page, Limit: limit, Period: period, + ForceRecompute: forceRecompute, ProjectFlockIDs: projectFlockIDs, AreaIDs: areaIDs, LocationIDs: locationIDs, diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index f34e2702..fac647f4 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -84,6 +84,7 @@ type ExpenseDepreciationQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"` Period string `query:"period" validate:"required,datetime=2006-01-02"` + ForceRecompute bool `query:"force_recompute"` ProjectFlockIDs []int64 `query:"-"` AreaIDs []int64 `query:"-"` LocationIDs []int64 `query:"-"`