diff --git a/internal/common/repository/common.hppv2.repository.go b/internal/common/repository/common.hppv2.repository.go index e437764a..787599e5 100644 --- a/internal/common/repository/common.hppv2.repository.go +++ b/internal/common/repository/common.hppv2.repository.go @@ -112,7 +112,7 @@ type HppV2CostRepository interface { GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) GetChickinPopulationByPFKForFarm(ctx context.Context, projectFlockID uint) (map[uint]float64, error) - GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) + GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, map[string]*time.Time, error) ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error) ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error) ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error) @@ -466,28 +466,30 @@ func (r *HppV2RepositoryImpl) GetMultiplicationPercentages( ctx context.Context, houseTypes []string, maxDay int, -) (map[string]map[int]float64, error) { +) (map[string]map[int]float64, map[string]*time.Time, error) { result := make(map[string]map[int]float64) + effectiveDates := make(map[string]*time.Time) if len(houseTypes) == 0 || maxDay <= 0 { - return result, nil + return result, effectiveDates, nil } type row struct { HouseType string Day int MultiplicationPercentage float64 + EffectiveDate *time.Time } rows := make([]row, 0) err := r.db.WithContext(ctx).Raw(` SELECT DISTINCT ON (house_type::text, day) - house_type::text AS house_type, day, multiplication_percentage + house_type::text AS house_type, day, multiplication_percentage, effective_date FROM house_depreciation_standards WHERE house_type::text IN ? AND day <= ? ORDER BY house_type, day, effective_date DESC NULLS LAST `, houseTypes, maxDay).Scan(&rows).Error if err != nil { - return nil, err + return nil, nil, err } for _, item := range rows { @@ -495,9 +497,12 @@ func (r *HppV2RepositoryImpl) GetMultiplicationPercentages( result[item.HouseType] = make(map[int]float64) } result[item.HouseType][item.Day] = item.MultiplicationPercentage + if _, tracked := effectiveDates[item.HouseType]; !tracked { + effectiveDates[item.HouseType] = item.EffectiveDate + } } - return result, nil + return result, effectiveDates, nil } func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags( diff --git a/internal/common/service/common.hppv2.service.go b/internal/common/service/common.hppv2.service.go index 76751bf6..5b32e446 100644 --- a/internal/common/service/common.hppv2.service.go +++ b/internal/common/service/common.hppv2.service.go @@ -1390,7 +1390,7 @@ func (s *hppV2Service) buildNormalTransferDepreciationPart( } houseType := NormalizeDepreciationHouseType(contextRow.HouseType) - multiplicationByHouseType, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, scheduleDay) + multiplicationByHouseType, effectiveDates, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, scheduleDay) if err != nil { return nil, err } @@ -1407,6 +1407,11 @@ func (s *hppV2Service) buildNormalTransferDepreciationPart( totalValueAfter := pulletCostDayN * multiplicationPercentage depreciationPercent := (1.0 - multiplicationPercentage) * 100.0 + var standardEffectiveDate string + if ed, ok := effectiveDates[houseType]; ok && ed != nil { + standardEffectiveDate = formatDateOnly(*ed) + } + return &HppV2ComponentPart{ Code: hppV2PartDepreciationNormal, Title: "Normal Transfer", @@ -1422,6 +1427,8 @@ func (s *hppV2Service) buildNormalTransferDepreciationPart( "origin_date": formatDateOnly(*originDate), "transfer_date": formatDateOnly(transferInput.TransferDate), "source_project_flock_id": transferInput.SourceProjectFlockID, + "standard_effective_date": standardEffectiveDate, + "kandang_population": transferInput.TransferQty, }, References: []HppV2Reference{ { @@ -1492,7 +1499,7 @@ func (s *hppV2Service) buildManualCutoverDepreciationPart( } houseType := NormalizeDepreciationHouseType(contextRow.HouseType) - multiplicationByHouseType, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, reportScheduleDay) + multiplicationByHouseType, effectiveDates, err := s.hppRepo.GetMultiplicationPercentages(context.Background(), []string{houseType}, reportScheduleDay) if err != nil { return nil, err } @@ -1511,6 +1518,11 @@ func (s *hppV2Service) buildManualCutoverDepreciationPart( depreciationPercent := (1.0 - multiplicationPercentage) * 100.0 _ = totalPulletCost + var standardEffectiveDate string + if ed, ok := effectiveDates[houseType]; ok && ed != nil { + standardEffectiveDate = formatDateOnly(*ed) + } + return &HppV2ComponentPart{ Code: hppV2PartDepreciationCutover, Title: "Manual Cut-over", @@ -1530,6 +1542,8 @@ func (s *hppV2Service) buildManualCutoverDepreciationPart( "cutover_date": formatDateOnly(manualInput.CutoverDate), "manual_input_id": manualInput.ID, "project_flock_kandang": projectFlockKandangId, + "standard_effective_date": standardEffectiveDate, + "kandang_population": kandangPopulation, }, References: []HppV2Reference{ { diff --git a/internal/common/service/common.hppv2.service_test.go b/internal/common/service/common.hppv2.service_test.go index 290abad8..346af535 100644 --- a/internal/common/service/common.hppv2.service_test.go +++ b/internal/common/service/common.hppv2.service_test.go @@ -103,8 +103,9 @@ func (s *hppV2RepoStub) GetDepreciationPercents(_ context.Context, houseTypes [] // GetMultiplicationPercentages — alias yang sama dengan GetDepreciationPercents untuk match // interface HppV2CostRepository (interface dipakai method name baru ini). -func (s *hppV2RepoStub) GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) { - return s.GetDepreciationPercents(ctx, houseTypes, maxDay) +func (s *hppV2RepoStub) GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, map[string]*time.Time, error) { + vals, err := s.GetDepreciationPercents(ctx, houseTypes, maxDay) + return vals, make(map[string]*time.Time), err } // GetChickinPopulationByPFKForFarm — return populasi per PFK dari satu project flock. diff --git a/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.down.sql b/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.down.sql deleted file mode 100644 index 288616ab..00000000 --- a/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Down migration: tidak ada cara restore TRUNCATE. Snapshot akan auto-regenerate on demand. --- File kosong sengaja: rollback safe karena snapshot dianggap cache yang bisa di-regenerate. -SELECT 1; diff --git a/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.up.sql b/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.up.sql deleted file mode 100644 index 627ea62d..00000000 --- a/internal/database/migrations/20260527074620_truncate_farm_depreciation_snapshots.up.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Truncate semua farm_depreciation_snapshots agar di-recompute dengan logic baru: --- 1. Multi-transfer per target kandang sekarang menghasilkan multiple parts (1 per transfer) --- 2. Economic cutoff date sudah diupdate dari 19 minggu ke 25 minggu --- 3. Format `components` JSON tetap kompatibel — yang berubah adalah jumlah entries (lebih banyak --- untuk kandang multi-transfer) --- --- Snapshot akan otomatis di-regenerate saat user request `/api/reports/expense/depreciation` --- untuk period yang relevan. - -TRUNCATE TABLE farm_depreciation_snapshots; diff --git a/internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.down.sql b/internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.down.sql similarity index 96% rename from internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.down.sql rename to internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.down.sql index d3619567..a7873191 100644 --- a/internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.down.sql +++ b/internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.down.sql @@ -1,6 +1,6 @@ -- Hapus open_house dan close_house rows dengan effective_date baru DELETE FROM house_depreciation_standards -WHERE house_type IN ('open_house', 'close_house') AND effective_date = '2026-05-20'; +WHERE house_type IN ('open_house', 'close_house') AND effective_date = '2026-05-29'; -- Hapus kolom multiplication_percentage ALTER TABLE house_depreciation_standards DROP COLUMN multiplication_percentage; diff --git a/internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.up.sql b/internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.up.sql similarity index 61% rename from internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.up.sql rename to internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.up.sql index e26831c1..59cb6d94 100644 --- a/internal/database/migrations/20260520000001_unify_close_house_depreciation_standard.up.sql +++ b/internal/database/migrations/20260529144454_unify_close_house_depreciation_standard.up.sql @@ -134,7 +134,7 @@ INSERT INTO house_depreciation_standards SELECT 'open_house'::house_type_enum, day, - '2026-05-20'::date, + '2026-05-29'::date, depreciation_percent, 25, 'Standard Open House Week 25', @@ -147,133 +147,26 @@ FROM ( ORDER BY day, effective_date DESC NULLS LAST ) effective_open_house; --- Insert close_house baru: depreciation_percent dari open_house, multiplication_percentage dari Excel row 8 (close_house) + +-- Insert close_house baru dengan effective_date 2026-05-29 +-- multiplication_percentage diambil dari row existing (sudah di-UPDATE di step sebelumnya) INSERT INTO house_depreciation_standards (house_type, day, effective_date, depreciation_percent, standard_week, name, multiplication_percentage) SELECT 'close_house'::house_type_enum, - oh.day, - '2026-05-20'::date, - oh.depreciation_percent, + day, + '2026-05-29'::date, + depreciation_percent, 25, 'Standard Close House Week 25', - ch.val + multiplication_percentage FROM ( SELECT DISTINCT ON (day) - day, depreciation_percent + day, depreciation_percent, multiplication_percentage FROM house_depreciation_standards WHERE house_type = 'open_house' ORDER BY day, effective_date DESC NULLS LAST -) oh -JOIN (VALUES - (1,0.9981),(2,0.9981),(3,0.9981),(4,0.9981),(5,0.9981), - (6,0.9981),(7,0.9981),(8,0.9978),(9,0.9978),(10,0.9978), - (11,0.9978),(12,0.9978),(13,0.9978),(14,0.9978),(15,0.9978), - (16,0.9978),(17,0.9978),(18,0.9978),(19,0.9978),(20,0.9978), - (21,0.9978),(22,0.9981),(23,0.9981),(24,0.9981),(25,0.9981), - (26,0.9981),(27,0.9981),(28,0.9981),(29,0.9978),(30,0.9978), - (31,0.9978),(32,0.9978),(33,0.9978),(34,0.9978),(35,0.9978), - (36,0.9978),(37,0.9978),(38,0.9978),(39,0.9978),(40,0.9978), - (41,0.9978),(42,0.9978),(43,0.9978),(44,0.9978),(45,0.9978), - (46,0.9978),(47,0.9978),(48,0.9978),(49,0.9978),(50,0.9981), - (51,0.9981),(52,0.9981),(53,0.9981),(54,0.9981),(55,0.9981), - (56,0.9981),(57,0.9978),(58,0.9978),(59,0.9978),(60,0.9978), - (61,0.9978),(62,0.9978),(63,0.9978),(64,0.9978),(65,0.9978), - (66,0.9977),(67,0.9977),(68,0.9977),(69,0.9977),(70,0.9977), - (71,0.9973),(72,0.9973),(73,0.9973),(74,0.9973),(75,0.9973), - (76,0.9973),(77,0.9973),(78,0.9977),(79,0.9977),(80,0.9977), - (81,0.9977),(82,0.9977),(83,0.9976),(84,0.9976),(85,0.9972), - (86,0.9972),(87,0.9972),(88,0.9972),(89,0.9972),(90,0.9972), - (91,0.9972),(92,0.9972),(93,0.9972),(94,0.9972),(95,0.9972), - (96,0.9972),(97,0.9972),(98,0.9971),(99,0.9975),(100,0.9975), - (101,0.9975),(102,0.9975),(103,0.9975),(104,0.9975),(105,0.9975), - (106,0.9971),(107,0.9971),(108,0.9971),(109,0.9971),(110,0.9971), - (111,0.997),(112,0.997),(113,0.9974),(114,0.9974),(115,0.9974), - (116,0.9974),(117,0.9974),(118,0.9974),(119,0.9974),(120,0.997), - (121,0.997),(122,0.997),(123,0.9969),(124,0.9969),(125,0.9969), - (126,0.9969),(127,0.9973),(128,0.9973),(129,0.9973),(130,0.9973), - (131,0.9973),(132,0.9973),(133,0.9973),(134,0.9968),(135,0.9968), - (136,0.9968),(137,0.9968),(138,0.9968),(139,0.9968),(140,0.9968), - (141,0.9972),(142,0.9972),(143,0.9972),(144,0.9972),(145,0.9972), - (146,0.9972),(147,0.9972),(148,0.9967),(149,0.9967),(150,0.9967), - (151,0.9967),(152,0.9967),(153,0.9967),(154,0.9966),(155,0.9971), - (156,0.9971),(157,0.9971),(158,0.9971),(159,0.9971),(160,0.9971), - (161,0.9971),(162,0.9971),(163,0.997),(164,0.997),(165,0.997), - (166,0.997),(167,0.997),(168,0.997),(169,0.9965),(170,0.9965), - (171,0.9965),(172,0.9965),(173,0.9964),(174,0.9964),(175,0.9964), - (176,0.9969),(177,0.9969),(178,0.9969),(179,0.9969),(180,0.9969), - (181,0.9969),(182,0.9969),(183,0.9968),(184,0.9968),(185,0.9968), - (186,0.9968),(187,0.9968),(188,0.9968),(189,0.9968),(190,0.9962), - (191,0.9962),(192,0.9962),(193,0.9962),(194,0.9962),(195,0.9962), - (196,0.9962),(197,0.9967),(198,0.9967),(199,0.9967),(200,0.9967), - (201,0.9966),(202,0.9966),(203,0.9966),(204,0.9966),(205,0.9966), - (206,0.9966),(207,0.9966),(208,0.9966),(209,0.9966),(210,0.9965), - (211,0.9965),(212,0.9965),(213,0.9965),(214,0.9965),(215,0.9965), - (216,0.9965),(217,0.9965),(218,0.9964),(219,0.9964),(220,0.9964), - (221,0.9964),(222,0.9964),(223,0.9964),(224,0.9964),(225,0.9957), - (226,0.9957),(227,0.9957),(228,0.9957),(229,0.9957),(230,0.9957), - (231,0.9956),(232,0.9962),(233,0.9962),(234,0.9962),(235,0.9962), - (236,0.9962),(237,0.9962),(238,0.9962),(239,0.9961),(240,0.9961), - (241,0.9961),(242,0.9961),(243,0.9961),(244,0.9961),(245,0.996), - (246,0.996),(247,0.996),(248,0.996),(249,0.996),(250,0.996), - (251,0.996),(252,0.9959),(253,0.9959),(254,0.9959),(255,0.9959), - (256,0.9959),(257,0.9959),(258,0.9958),(259,0.9958),(260,0.9958), - (261,0.9958),(262,0.9958),(263,0.9957),(264,0.9957),(265,0.9957), - (266,0.9957),(267,0.9957),(268,0.9957),(269,0.9956),(270,0.9956), - (271,0.9956),(272,0.9956),(273,0.9956),(274,0.9955),(275,0.9955), - (276,0.9955),(277,0.9955),(278,0.9955),(279,0.9954),(280,0.9954), - (281,0.9954),(282,0.9954),(283,0.9953),(284,0.9953),(285,0.9953), - (286,0.9953),(287,0.9953),(288,0.996),(289,0.996),(290,0.996), - (291,0.996),(292,0.996),(293,0.996),(294,0.9959),(295,0.9951), - (296,0.9951),(297,0.9951),(298,0.995),(299,0.995),(300,0.995), - (301,0.995),(302,0.9949),(303,0.9949),(304,0.9949),(305,0.9948), - (306,0.9948),(307,0.9948),(308,0.9948),(309,0.9947),(310,0.9947), - (311,0.9947),(312,0.9947),(313,0.9946),(314,0.9946),(315,0.9946), - (316,0.9945),(317,0.9945),(318,0.9945),(319,0.9944),(320,0.9944), - (321,0.9944),(322,0.9944),(323,0.9953),(324,0.9952),(325,0.9952), - (326,0.9952),(327,0.9952),(328,0.9952),(329,0.9951),(330,0.9941), - (331,0.9941),(332,0.9941),(333,0.994),(334,0.994),(335,0.994), - (336,0.9939),(337,0.9949),(338,0.9949),(339,0.9948),(340,0.9948), - (341,0.9948),(342,0.9948),(343,0.9947),(344,0.9937),(345,0.9936), - (346,0.9936),(347,0.9935),(348,0.9935),(349,0.9934),(350,0.9934), - (351,0.9934),(352,0.9933),(353,0.9933),(354,0.9932),(355,0.9932), - (356,0.9931),(357,0.9931),(358,0.9942),(359,0.9942),(360,0.9941), - (361,0.9941),(362,0.9941),(363,0.994),(364,0.994),(365,0.9927), - (366,0.9927),(367,0.9926),(368,0.9926),(369,0.9925),(370,0.9925), - (371,0.9924),(372,0.9936),(373,0.9936),(374,0.9935),(375,0.9935), - (376,0.9935),(377,0.9934),(378,0.9934),(379,0.9933),(380,0.9933), - (381,0.9932),(382,0.9932),(383,0.9931),(384,0.9931),(385,0.993), - (386,0.9916),(387,0.9915),(388,0.9915),(389,0.9914),(390,0.9913), - (391,0.9912),(392,0.9912),(393,0.9926),(394,0.9925),(395,0.9924), - (396,0.9924),(397,0.9923),(398,0.9923),(399,0.9922),(400,0.9922), - (401,0.9921),(402,0.992),(403,0.992),(404,0.9919),(405,0.9918), - (406,0.9918),(407,0.9917),(408,0.9916),(409,0.9916),(410,0.9915), - (411,0.9914),(412,0.9913),(413,0.9913),(414,0.9894),(415,0.9893), - (416,0.9892),(417,0.9891),(418,0.989),(419,0.9888),(420,0.9887), - (421,0.9905),(422,0.9904),(423,0.9903),(424,0.9902),(425,0.9901), - (426,0.99),(427,0.9899),(428,0.9898),(429,0.9897),(430,0.9896), - (431,0.9895),(432,0.9894),(433,0.9892),(434,0.9891),(435,0.989), - (436,0.9889),(437,0.9888),(438,0.9886),(439,0.9885),(440,0.9884), - (441,0.9882),(442,0.9881),(443,0.988),(444,0.9878),(445,0.9877), - (446,0.9875),(447,0.9873),(448,0.9872),(449,0.987),(450,0.9868), - (451,0.9867),(452,0.9865),(453,0.9863),(454,0.9861),(455,0.9859), - (456,0.9857),(457,0.9855),(458,0.9853),(459,0.9851),(460,0.9848), - (461,0.9846),(462,0.9844),(463,0.9873),(464,0.9871),(465,0.987), - (466,0.9868),(467,0.9866),(468,0.9864),(469,0.9863),(470,0.9826), - (471,0.9823),(472,0.9819),(473,0.9816),(474,0.9813),(475,0.9809), - (476,0.9805),(477,0.9802),(478,0.9798),(479,0.9793),(480,0.9789), - (481,0.9784),(482,0.978),(483,0.9775),(484,0.977),(485,0.9764), - (486,0.9758),(487,0.9752),(488,0.9746),(489,0.974),(490,0.9733), - (491,0.978),(492,0.9775),(493,0.977),(494,0.9765),(495,0.9759), - (496,0.9753),(497,0.9747),(498,0.9675),(499,0.9664),(500,0.9653), - (501,0.964),(502,0.9627),(503,0.9612),(504,0.9597),(505,0.9664), - (506,0.9652),(507,0.964),(508,0.9626),(509,0.9612),(510,0.9596), - (511,0.9579),(512,0.9451),(513,0.9419),(514,0.9383),(515,0.9342), - (516,0.9296),(517,0.9242),(518,0.918),(519,0.9286),(520,0.9231), - (521,0.9167),(522,0.9091),(523,0.9),(524,0.8889),(525,0.875), - (526,0.8571),(527,0.8333),(528,0.8),(529,0.75),(530,0.6667), - (531,0.5),(532,0) -) AS ch(day, val) ON oh.day = ch.day; +) effective_close_house; -- Invalidate snapshot cache depreciation agar recompute dengan standard baru DELETE FROM farm_depreciation_snapshots; diff --git a/internal/database/migrations/20260527074540_recalculate_economic_cutoff_25_weeks.down.sql b/internal/database/migrations/20260529144531_recalculate_economic_cutoff_25_weeks.down.sql similarity index 100% rename from internal/database/migrations/20260527074540_recalculate_economic_cutoff_25_weeks.down.sql rename to internal/database/migrations/20260529144531_recalculate_economic_cutoff_25_weeks.down.sql diff --git a/internal/database/migrations/20260527074540_recalculate_economic_cutoff_25_weeks.up.sql b/internal/database/migrations/20260529144531_recalculate_economic_cutoff_25_weeks.up.sql similarity index 100% rename from internal/database/migrations/20260527074540_recalculate_economic_cutoff_25_weeks.up.sql rename to internal/database/migrations/20260529144531_recalculate_economic_cutoff_25_weeks.up.sql diff --git a/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.down.sql b/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.down.sql new file mode 100644 index 00000000..aca64495 --- /dev/null +++ b/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.down.sql @@ -0,0 +1,14 @@ +-- Rollback total_cost ke nilai sebelum migration +UPDATE farm_depreciation_manual_inputs +SET total_cost = 562618200.000, + updated_at = NOW() +WHERE project_flock_id = 10; + +UPDATE farm_depreciation_manual_inputs +SET total_cost = 598552406.000, + updated_at = NOW() +WHERE project_flock_id = 11; + +-- Snapshot lama tidak bisa di-restore — biarkan kosong, recompute otomatis +-- saat user request endpoint depresiasi +TRUNCATE TABLE farm_depreciation_snapshots; diff --git a/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.up.sql b/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.up.sql new file mode 100644 index 00000000..446296ea --- /dev/null +++ b/internal/database/migrations/20260529144559_update_farm_depreciation_manual_inputs_pfk_10_11.up.sql @@ -0,0 +1,21 @@ +-- Update total_cost farm_depreciation_manual_inputs untuk PFK 10 & 11 +-- per permintaan user (cutover 28 Feb 2026) +-- +-- PFK 10 (Flock Jamali 003) : 562.618.200,000 -> 1.900.157.533,55 +-- PFK 11 (Flock Tamansari 001) : 598.552.406,000 -> 2.521.797.832,14 + +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 = 2521797832.14, + cutover_date = DATE '2026-02-28', + updated_at = NOW() +WHERE project_flock_id = 11; + +-- 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/repports/dto/repportExpenseDepreciation.dto.go b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go index 202e61d1..db6328cd 100644 --- a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go +++ b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go @@ -26,6 +26,8 @@ type ExpenseDepreciationRowDTO struct { DayN int `json:"day_n"` ChickinDate string `json:"chickin_date"` TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"` + StandardEffectiveDate string `json:"standard_effective_date,omitempty"` + TotalPopulation float64 `json:"total_population"` Components any `json:"components"` } diff --git a/internal/modules/repports/repositories/expense_depreciation.repository.go b/internal/modules/repports/repositories/expense_depreciation.repository.go index 054fa8dd..39223b83 100644 --- a/internal/modules/repports/repositories/expense_depreciation.repository.go +++ b/internal/modules/repports/repositories/expense_depreciation.repository.go @@ -41,6 +41,7 @@ type houseMultiplicationPercentageRow struct { HouseType string Day int MultiplicationPercentage float64 + EffectiveDate *time.Time } type ExpenseDepreciationRepository interface { @@ -50,7 +51,7 @@ type ExpenseDepreciationRepository interface { DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error DeleteSnapshotsByFarmIDs(ctx context.Context, farmIDs []uint) error GetLatestTransferInputsByFarms(ctx context.Context, period time.Time, farmIDs []uint) ([]FarmDepreciationLatestTransferRow, error) - GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) + GetMultiplicationPercentages(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, map[string]*time.Time, error) GetLatestManualInputsByFarms(ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationManualInputRow, error) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error DB() *gorm.DB @@ -244,21 +245,22 @@ func (r *expenseDepreciationRepository) GetMultiplicationPercentages( ctx context.Context, houseTypes []string, maxDay int, -) (map[string]map[int]float64, error) { +) (map[string]map[int]float64, map[string]*time.Time, error) { result := make(map[string]map[int]float64) + effectiveDates := make(map[string]*time.Time) if len(houseTypes) == 0 || maxDay <= 0 { - return result, nil + return result, effectiveDates, nil } rows := make([]houseMultiplicationPercentageRow, 0) if err := r.db.WithContext(ctx).Raw(` SELECT DISTINCT ON (house_type::text, day) - house_type::text AS house_type, day, multiplication_percentage + house_type::text AS house_type, day, multiplication_percentage, effective_date FROM house_depreciation_standards WHERE house_type::text IN ? AND day <= ? ORDER BY house_type, day, effective_date DESC NULLS LAST `, houseTypes, maxDay).Scan(&rows).Error; err != nil { - return nil, err + return nil, nil, err } for _, row := range rows { @@ -266,9 +268,12 @@ func (r *expenseDepreciationRepository) GetMultiplicationPercentages( result[row.HouseType] = make(map[int]float64) } result[row.HouseType][row.Day] = row.MultiplicationPercentage + if _, tracked := effectiveDates[row.HouseType]; !tracked { + effectiveDates[row.HouseType] = row.EffectiveDate + } } - return result, nil + return result, effectiveDates, nil } func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms( diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 54b30f64..198dfa82 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -304,7 +304,8 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe continue } components := parseSnapshotComponents(snapshot.Components) - multiplicationPercentage, dayN, chickinDate := depreciationSnapshotInfo(components) + multiplicationPercentage, dayN, chickinDate, standardEffectiveDate := depreciationSnapshotInfo(components) + totalPopulation := depreciationTotalPopulation(components) rows = append(rows, dto.ExpenseDepreciationRowDTO{ ProjectFlockID: int64(snapshot.ProjectFlockId), FarmName: candidate.FarmName, @@ -316,6 +317,8 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe DayN: dayN, ChickinDate: chickinDate, TotalValuePulletAfterDepreciation: snapshot.PulletCostDayNTotal - snapshot.DepreciationValue, + StandardEffectiveDate: standardEffectiveDate, + TotalPopulation: totalPopulation, Components: components, }) } @@ -502,11 +505,14 @@ type depreciationKandangComponent struct { OriginDate string `json:"origin_date,omitempty"` ChickinDate string `json:"chickin_date,omitempty"` StartScheduleDay *int `json:"start_schedule_day,omitempty"` + StandardEffectiveDate string `json:"standard_effective_date,omitempty"` + Population float64 `json:"population"` } type depreciationFarmComponents struct { - KandangCount int `json:"kandang_count"` - Kandang []depreciationKandangComponent `json:"kandang"` + KandangCount int `json:"kandang_count"` + TotalPopulation float64 `json:"total_population"` + Kandang []depreciationKandangComponent `json:"kandang"` } func (s *repportService) computeExpenseDepreciationSnapshots( @@ -540,6 +546,7 @@ func (s *repportService) computeExpenseDepreciationSnapshots( totalDepreciationValue := 0.0 totalPulletCostDayN := 0.0 + totalPopulation := 0.0 for _, kandangID := range kandangIDs { breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate) if err != nil { @@ -575,6 +582,8 @@ func (s *repportService) computeExpenseDepreciationSnapshots( DepreciationSource: part.Code, OriginDate: hppV2DetailString(part.Details, "origin_date"), ChickinDate: hppV2DetailString(part.Details, "origin_date"), + StandardEffectiveDate: hppV2DetailString(part.Details, "standard_effective_date"), + Population: hppV2DetailFloat(part.Details, "kandang_population"), } if component.HouseType == "" { @@ -605,11 +614,13 @@ func (s *repportService) computeExpenseDepreciationSnapshots( totalPulletCostDayN += component.PulletCostDayN totalDepreciationValue += component.DepreciationValue + totalPopulation += component.Population components.Kandang = append(components.Kandang, component) } } components.KandangCount = len(components.Kandang) + components.TotalPopulation = totalPopulation effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) componentsJSON, marshalErr := json.Marshal(components) @@ -744,14 +755,14 @@ func parseSnapshotComponents(raw []byte) any { return out } -func depreciationSnapshotInfo(components any) (float64, int, string) { +func depreciationSnapshotInfo(components any) (float64, int, string, string) { root, ok := components.(map[string]any) if !ok { - return 0, 0, "" + return 0, 0, "", "" } kandang, ok := root["kandang"].([]any) if !ok { - return 0, 0, "" + return 0, 0, "", "" } for _, raw := range kandang { component, ok := raw.(map[string]any) @@ -765,10 +776,19 @@ func depreciationSnapshotInfo(components any) (float64, int, string) { chickinDate = anyString(component["origin_date"]) } if dayN > 0 || multiplicationPercentage > 0 || chickinDate != "" { - return multiplicationPercentage, dayN, chickinDate + standardEffectiveDate := anyString(component["standard_effective_date"]) + return multiplicationPercentage, dayN, chickinDate, standardEffectiveDate } } - return 0, 0, "" + return 0, 0, "", "" +} + +func depreciationTotalPopulation(components any) float64 { + root, ok := components.(map[string]any) + if !ok { + return 0 + } + return anyFloat(root["total_population"]) } func anyFloat(raw any) float64 { @@ -1823,15 +1843,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu return nil, 0, err } - expenseIDs := make([]uint64, 0, len(expenses)) - for _, exp := range expenses { - expenseIDs = append(expenseIDs, exp.Id) - } - expenseWarehousesMap, err := s.DebtSupplierRepo.GetWarehousesByExpenseIDs(c.Context(), expenseIDs) - if err != nil { - return nil, 0, err - } - purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs)) for _, purchase := range purchases { purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase) @@ -1939,7 +1950,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu } for _, exp := range expensesBySupplier[supplierID] { - row := buildDebtSupplierExpenseRow(exp, expenseWarehousesMap[exp.Id], now, location) + row := buildDebtSupplierExpenseRow(exp, now, location) sortTime := exp.TransactionDate.In(location) rowIndex := len(combinedRows) combinedRows = append(combinedRows, debtSupplierRowItem{ @@ -2111,6 +2122,12 @@ func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Loc poDate = purchase.PoDate.In(loc).Format("2006-01-02") } + var firstWarehouse *warehouseDTO.WarehouseRelationDTO + if len(warehouses) > 0 { + w := warehouses[0] + firstWarehouse = &w + } + return dto.DebtSupplierRowDTO{ PrNumber: prNumber, PoNumber: poNumber, @@ -2118,7 +2135,7 @@ func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Loc ReceivedDate: receivedDate, Aging: aging, Area: area, - Warehouses: warehouses, + Warehouse: firstWarehouse, DueDate: dueDate, DueStatus: dueStatus, TotalPrice: totalPrice, @@ -2148,7 +2165,7 @@ func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"), Aging: 0, Area: nil, - Warehouses: []warehouseDTO.WarehouseRelationDTO{}, + Warehouse: nil, DueDate: "-", DueStatus: "-", TotalPrice: 0, @@ -2361,7 +2378,7 @@ func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Locatio return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc) } -func buildDebtSupplierExpenseRow(exp entity.Expense, warehouses []entity.Warehouse, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { +func buildDebtSupplierExpenseRow(exp entity.Expense, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { txDate := exp.TransactionDate.In(loc) dateStr := txDate.Format("2006-01-02") @@ -2383,15 +2400,6 @@ func buildDebtSupplierExpenseRow(exp entity.Expense, warehouses []entity.Warehou area = &mapped } - warehouseDTOs := make([]warehouseDTO.WarehouseRelationDTO, 0, len(warehouses)) - seenWarehouseIDs := map[uint]bool{} - for _, w := range warehouses { - if w.Id != 0 && !seenWarehouseIDs[w.Id] { - seenWarehouseIDs[w.Id] = true - warehouseDTOs = append(warehouseDTOs, warehouseDTO.ToWarehouseRelationDTO(w)) - } - } - poNumber := "" if strings.TrimSpace(exp.PoNumber) != "" { poNumber = exp.PoNumber @@ -2404,7 +2412,7 @@ func buildDebtSupplierExpenseRow(exp entity.Expense, warehouses []entity.Warehou ReceivedDate: dateStr, Aging: aging, Area: area, - Warehouses: warehouseDTOs, + Warehouse: nil, DueDate: "-", DueStatus: "-", TotalPrice: totalPrice,