diff --git a/internal/common/repository/common.hppv2.repository.go b/internal/common/repository/common.hppv2.repository.go index 75bed362..75a324ec 100644 --- a/internal/common/repository/common.hppv2.repository.go +++ b/internal/common/repository/common.hppv2.repository.go @@ -96,6 +96,7 @@ type HppV2FarmDepreciationSnapshotRow struct { DepreciationPercentEffective float64 DepreciationValue float64 PulletCostDayNTotal float64 + Components []byte } type HppV2CostRepository interface { @@ -404,7 +405,7 @@ func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeri var row HppV2FarmDepreciationSnapshotRow err := r.db.WithContext(ctx). Table("farm_depreciation_snapshots"). - Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total"). + Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total, components"). Where("project_flock_id = ?", projectFlockID). Where("period_date = DATE(?)", periodDate). Limit(1). diff --git a/internal/common/service/common.hppv2.service.go b/internal/common/service/common.hppv2.service.go index fb760cfe..f620d442 100644 --- a/internal/common/service/common.hppv2.service.go +++ b/internal/common/service/common.hppv2.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -1472,6 +1473,18 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart( depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100 } + details := map[string]any{ + "basis_total": snapshot.DepreciationValue, + "pullet_cost_day_n": appliedPulletCostDayN, + "depreciation_percent": depreciationPercent, + "snapshot_id": snapshot.ID, + "snapshot_period_date": formatDateOnly(snapshot.PeriodDate), + "snapshot_project_flock": snapshot.ProjectFlockID, + } + for key, value := range farmDepreciationSnapshotMetadata(snapshot.Components, projectFlockKandangId) { + details[key] = value + } + return &HppV2ComponentPart{ Code: hppV2PartDepreciationFarmSnapshot, Title: "Farm Snapshot", @@ -1483,14 +1496,7 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart( Denominator: denominator, Ratio: ratio, }, - Details: map[string]any{ - "basis_total": snapshot.DepreciationValue, - "pullet_cost_day_n": appliedPulletCostDayN, - "depreciation_percent": depreciationPercent, - "snapshot_id": snapshot.ID, - "snapshot_period_date": formatDateOnly(snapshot.PeriodDate), - "snapshot_project_flock": snapshot.ProjectFlockID, - }, + Details: details, References: []HppV2Reference{ { Type: "farm_depreciation_snapshot", @@ -1504,6 +1510,84 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart( }, nil } +type farmDepreciationSnapshotComponents struct { + Kandang []farmDepreciationSnapshotKandangComponent `json:"kandang"` +} + +type farmDepreciationSnapshotKandangComponent struct { + ProjectFlockKandangID uint `json:"project_flock_kandang_id"` + DayN int `json:"day_n"` + MultiplicationPercent float64 `json:"multiplication_percentage"` + ChickinDate string `json:"chickin_date"` + OriginDate string `json:"origin_date"` + StandardEffectiveDate string `json:"standard_effective_date"` + Population float64 `json:"population"` +} + +func farmDepreciationSnapshotMetadata(raw []byte, projectFlockKandangID uint) map[string]any { + result := make(map[string]any) + if len(raw) == 0 { + return result + } + + var components farmDepreciationSnapshotComponents + if err := json.Unmarshal(raw, &components); err != nil { + return result + } + + var fallback *farmDepreciationSnapshotKandangComponent + for i := range components.Kandang { + component := &components.Kandang[i] + if !component.hasDepreciationMetadata() { + continue + } + if component.ProjectFlockKandangID == projectFlockKandangID { + return component.snapshotDetails() + } + if fallback == nil { + fallback = component + } + } + if fallback != nil { + return fallback.snapshotDetails() + } + + return result +} + +func (c farmDepreciationSnapshotKandangComponent) hasDepreciationMetadata() bool { + return c.DayN > 0 || + c.MultiplicationPercent > 0 || + c.ChickinDate != "" || + c.OriginDate != "" || + c.StandardEffectiveDate != "" || + c.Population > 0 +} + +func (c farmDepreciationSnapshotKandangComponent) snapshotDetails() map[string]any { + chickinDate := c.ChickinDate + if chickinDate == "" { + chickinDate = c.OriginDate + } + + details := map[string]any{ + "schedule_day": c.DayN, + "multiplication_percentage": c.MultiplicationPercent, + } + if chickinDate != "" { + details["origin_date"] = chickinDate + details["chickin_date"] = chickinDate + } + if c.StandardEffectiveDate != "" { + details["standard_effective_date"] = c.StandardEffectiveDate + } + if c.Population > 0 { + details["kandang_population"] = c.Population + } + + return details +} + func (s *hppV2Service) buildNormalTransferDepreciationPart( contextRow *commonRepo.HppV2ProjectFlockKandangContext, transferInput *commonRepo.HppV2LatestTransferInputRow, diff --git a/internal/common/service/common.hppv2.service_test.go b/internal/common/service/common.hppv2.service_test.go index b628acad..af8f7d3d 100644 --- a/internal/common/service/common.hppv2.service_test.go +++ b/internal/common/service/common.hppv2.service_test.go @@ -825,6 +825,28 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro DepreciationPercentEffective: 10, DepreciationValue: 1000, PulletCostDayNTotal: 10000, + Components: []byte(`{ + "kandang_count": 2, + "total_population": 1000, + "kandang": [ + { + "project_flock_kandang_id": 71, + "day_n": 5, + "multiplication_percentage": 0.95, + "chickin_date": "2026-01-02", + "standard_effective_date": "2026-06-01", + "population": 800 + }, + { + "project_flock_kandang_id": 70, + "day_n": 7, + "multiplication_percentage": 0.93, + "chickin_date": "2026-01-01", + "standard_effective_date": "2026-06-02", + "population": 200 + } + ] + }`), }, }, eggProductionByPFK: map[uint]struct { @@ -873,6 +895,21 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro if depreciation.Parts[0].Details["snapshot_id"] != uint(901) { t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details) } + if depreciation.Parts[0].Details["schedule_day"] != 7 { + t.Fatalf("expected snapshot schedule_day 7, got %+v", depreciation.Parts[0].Details) + } + if depreciation.Parts[0].Details["multiplication_percentage"] != 0.93 { + t.Fatalf("expected snapshot multiplication_percentage 0.93, got %+v", depreciation.Parts[0].Details) + } + if depreciation.Parts[0].Details["chickin_date"] != "2026-01-01" { + t.Fatalf("expected snapshot chickin_date 2026-01-01, got %+v", depreciation.Parts[0].Details) + } + if depreciation.Parts[0].Details["standard_effective_date"] != "2026-06-02" { + t.Fatalf("expected snapshot standard_effective_date 2026-06-02, got %+v", depreciation.Parts[0].Details) + } + if depreciation.Parts[0].Details["kandang_population"] != float64(200) { + t.Fatalf("expected snapshot kandang_population 200, got %+v", depreciation.Parts[0].Details) + } } func stubKey(ids []uint, flags []string) string { diff --git a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go index 2df82a5f..80b61ed4 100644 --- a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go +++ b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go @@ -50,7 +50,7 @@ type ExpenseDepreciationV2MetaDTO struct { } type ExpenseDepreciationV2RowDTO struct { - Date string `json:"date"` + Date string `json:"date"` DepreciationPercentEffective float64 `json:"depreciation_percent_effective"` DepreciationValue float64 `json:"depreciation_value"` PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"` @@ -60,7 +60,6 @@ type ExpenseDepreciationV2RowDTO struct { TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"` StandardEffectiveDate string `json:"standard_effective_date,omitempty"` TotalPopulation float64 `json:"total_population"` - Components any `json:"components"` } func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO { diff --git a/internal/modules/repports/dto/repportExpenseDepreciation_test.go b/internal/modules/repports/dto/repportExpenseDepreciation_test.go new file mode 100644 index 00000000..649bc234 --- /dev/null +++ b/internal/modules/repports/dto/repportExpenseDepreciation_test.go @@ -0,0 +1,51 @@ +package dto + +import ( + "encoding/json" + "testing" +) + +func TestExpenseDepreciationRowDTOComponentsJSONContract(t *testing.T) { + v1 := ExpenseDepreciationRowDTO{ + ProjectFlockID: 1, + FarmName: "Farm A", + Period: "2026-06-05", + Components: map[string]any{"kandang_count": 1}, + } + rawV1, err := json.Marshal(v1) + if err != nil { + t.Fatalf("marshal v1 dto: %v", err) + } + + var decodedV1 map[string]any + if err := json.Unmarshal(rawV1, &decodedV1); err != nil { + t.Fatalf("unmarshal v1 dto: %v", err) + } + if _, ok := decodedV1["components"]; !ok { + t.Fatalf("expected v1 components to be present, got %s", string(rawV1)) + } + + v2 := ExpenseDepreciationV2RowDTO{ + Date: "2026-06-05", + DepreciationPercentEffective: 10, + DepreciationValue: 100, + PulletCostDayNTotal: 1000, + MultiplicationPercentage: 0.9, + DayN: 2, + ChickinDate: "2026-01-01", + TotalValuePulletAfterDepreciation: 900, + TotalPopulation: 100, + } + rawV2, err := json.Marshal(v2) + if err != nil { + t.Fatalf("marshal v2 dto: %v", err) + } + + var decodedV2 map[string]any + if err := json.Unmarshal(rawV2, &decodedV2); err != nil { + t.Fatalf("unmarshal v2 dto: %v", err) + } + if _, ok := decodedV2["components"]; ok { + t.Fatalf("expected v2 components to be omitted, got %s", string(rawV2)) + } +} diff --git a/internal/modules/repports/services/repport.expense_depreciation_test.go b/internal/modules/repports/services/repport.expense_depreciation_test.go index 3f10e428..645c0f1a 100644 --- a/internal/modules/repports/services/repport.expense_depreciation_test.go +++ b/internal/modules/repports/services/repport.expense_depreciation_test.go @@ -90,6 +90,12 @@ func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, return nil } +func (m *expenseDepreciationRepoMock) DeleteSnapshotsByFarmIDs(_ context.Context, farmIDs []uint) error { + m.deleteCalled = true + m.deleteFarmIDs = append([]uint{}, farmIDs...) + return nil +} + func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) { return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 21f72e8c..a3f95170 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -423,7 +423,10 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense var totalDepreciationValue float64 var totalPulletCostDayN float64 var totalPopulation float64 - var allKandangComponents []depreciationKandangComponent + var multiplicationPercentage float64 + var dayN int + var chickinDate string + var standardEffectiveDate string for _, kandangID := range kandangIDs { breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate) @@ -444,72 +447,33 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense continue } - houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType) - component := depreciationKandangComponent{ - ProjectFlockKandangID: breakdown.ProjectFlockKandangID, - KandangID: breakdown.KandangID, - KandangName: breakdown.KandangName, - SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"), - HouseType: houseType, - DayN: hppV2DetailInt(part.Details, "schedule_day"), - DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"), - MultiplicationPercentage: hppV2DetailFloat(part.Details, "multiplication_percentage"), - PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"), - DepreciationValue: part.Total, - TotalValuePulletAfterDepreciation: hppV2DetailFloat(part.Details, "total_value_pullet_after_depreciation"), - 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"), + partPulletCostDayN := hppV2DetailFloat(part.Details, "pullet_cost_day_n") + partPopulation := hppV2DetailFloat(part.Details, "kandang_population") + partDayN := hppV2DetailInt(part.Details, "schedule_day") + partMultiplicationPercentage := hppV2DetailFloat(part.Details, "multiplication_percentage") + partChickinDate := hppV2DetailString(part.Details, "chickin_date") + if partChickinDate == "" { + partChickinDate = hppV2DetailString(part.Details, "origin_date") } - if component.HouseType == "" { - component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type")) - } + totalPulletCostDayN += partPulletCostDayN + totalDepreciationValue += part.Total + totalPopulation += partPopulation - if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil { - component.TransferID = ref.ID - component.TransferDate = ref.Date - component.TransferQty = ref.Qty + if dayN == 0 && multiplicationPercentage == 0 && chickinDate == "" && + (partDayN > 0 || partMultiplicationPercentage > 0 || partChickinDate != "") { + dayN = partDayN + multiplicationPercentage = partMultiplicationPercentage + chickinDate = partChickinDate + standardEffectiveDate = hppV2DetailString(part.Details, "standard_effective_date") } - - if part.Code == "manual_cutover" { - if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 { - component.StartScheduleDay = &startDay - } - component.CutoverDate = hppV2DetailString(part.Details, "cutover_date") - if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 { - component.ManualInputID = &manualID - } - if component.ManualInputID == nil { - if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 { - manualID := ref.ID - component.ManualInputID = &manualID - } - } - } - - totalPulletCostDayN += component.PulletCostDayN - totalDepreciationValue += component.DepreciationValue - totalPopulation += component.Population - allKandangComponents = append(allKandangComponents, component) } } effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) - components := depreciationFarmComponents{ - KandangCount: len(allKandangComponents), - TotalPopulation: totalPopulation, - Kandang: allKandangComponents, - } - componentsJSON, _ := json.Marshal(components) - - multiplicationPercentage, dayN, chickinDate, standardEffectiveDate := depreciationSnapshotInfo(parseSnapshotComponents(componentsJSON)) - rows = append(rows, dto.ExpenseDepreciationV2RowDTO{ - Date: dayStr, + Date: dayStr, DepreciationPercentEffective: effectivePercent, DepreciationValue: totalDepreciationValue, PulletCostDayNTotal: totalPulletCostDayN, @@ -519,7 +483,6 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue, StandardEffectiveDate: standardEffectiveDate, TotalPopulation: totalPopulation, - Components: parseSnapshotComponents(componentsJSON), }) actualDays++ }