Compare commits

...

2 Commits

Author SHA1 Message Date
giovanni 9ab4e1a6ef adjust total bobot laporan keuangan 2026-06-08 12:37:39 +07:00
giovanni 217f35b250 adjust response depretitation v2 2026-06-08 12:30:51 +07:00
8 changed files with 211 additions and 70 deletions
@@ -96,6 +96,7 @@ type HppV2FarmDepreciationSnapshotRow struct {
DepreciationPercentEffective float64 DepreciationPercentEffective float64
DepreciationValue float64 DepreciationValue float64
PulletCostDayNTotal float64 PulletCostDayNTotal float64
Components []byte
} }
type HppV2CostRepository interface { type HppV2CostRepository interface {
@@ -404,7 +405,7 @@ func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeri
var row HppV2FarmDepreciationSnapshotRow var row HppV2FarmDepreciationSnapshotRow
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("farm_depreciation_snapshots"). 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("project_flock_id = ?", projectFlockID).
Where("period_date = DATE(?)", periodDate). Where("period_date = DATE(?)", periodDate).
Limit(1). Limit(1).
@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"encoding/json"
"time" "time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -1472,6 +1473,18 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100 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{ return &HppV2ComponentPart{
Code: hppV2PartDepreciationFarmSnapshot, Code: hppV2PartDepreciationFarmSnapshot,
Title: "Farm Snapshot", Title: "Farm Snapshot",
@@ -1483,14 +1496,7 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
Denominator: denominator, Denominator: denominator,
Ratio: ratio, Ratio: ratio,
}, },
Details: map[string]any{ Details: details,
"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,
},
References: []HppV2Reference{ References: []HppV2Reference{
{ {
Type: "farm_depreciation_snapshot", Type: "farm_depreciation_snapshot",
@@ -1504,6 +1510,84 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
}, nil }, 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( func (s *hppV2Service) buildNormalTransferDepreciationPart(
contextRow *commonRepo.HppV2ProjectFlockKandangContext, contextRow *commonRepo.HppV2ProjectFlockKandangContext,
transferInput *commonRepo.HppV2LatestTransferInputRow, transferInput *commonRepo.HppV2LatestTransferInputRow,
@@ -825,6 +825,28 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
DepreciationPercentEffective: 10, DepreciationPercentEffective: 10,
DepreciationValue: 1000, DepreciationValue: 1000,
PulletCostDayNTotal: 10000, 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 { eggProductionByPFK: map[uint]struct {
@@ -873,6 +895,21 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) { if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details) 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 { func stubKey(ids []uint, flags []string) string {
@@ -60,7 +60,6 @@ type ExpenseDepreciationV2RowDTO struct {
TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"` TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"`
StandardEffectiveDate string `json:"standard_effective_date,omitempty"` StandardEffectiveDate string `json:"standard_effective_date,omitempty"`
TotalPopulation float64 `json:"total_population"` TotalPopulation float64 `json:"total_population"`
Components any `json:"components"`
} }
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO { func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
@@ -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))
}
}
@@ -83,7 +83,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppByDeliver
realizationDate = *mdp.DeliveryDate realizationDate = *mdp.DeliveryDate
} }
totalWeightKg := mdp.UsageQty * mdp.AvgWeight totalWeightKg := mdp.TotalWeight
salesAmount := totalWeightKg * mdp.UnitPrice salesAmount := totalWeightKg * mdp.UnitPrice
var hpp float64 var hpp float64
@@ -90,6 +90,12 @@ func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context,
return nil 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) { func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
} }
@@ -423,7 +423,10 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
var totalDepreciationValue float64 var totalDepreciationValue float64
var totalPulletCostDayN float64 var totalPulletCostDayN float64
var totalPopulation float64 var totalPopulation float64
var allKandangComponents []depreciationKandangComponent var multiplicationPercentage float64
var dayN int
var chickinDate string
var standardEffectiveDate string
for _, kandangID := range kandangIDs { for _, kandangID := range kandangIDs {
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate) breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
@@ -444,70 +447,31 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
continue continue
} }
houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType) partPulletCostDayN := hppV2DetailFloat(part.Details, "pullet_cost_day_n")
component := depreciationKandangComponent{ partPopulation := hppV2DetailFloat(part.Details, "kandang_population")
ProjectFlockKandangID: breakdown.ProjectFlockKandangID, partDayN := hppV2DetailInt(part.Details, "schedule_day")
KandangID: breakdown.KandangID, partMultiplicationPercentage := hppV2DetailFloat(part.Details, "multiplication_percentage")
KandangName: breakdown.KandangName, partChickinDate := hppV2DetailString(part.Details, "chickin_date")
SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"), if partChickinDate == "" {
HouseType: houseType, partChickinDate = hppV2DetailString(part.Details, "origin_date")
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"),
} }
if component.HouseType == "" { totalPulletCostDayN += partPulletCostDayN
component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type")) totalDepreciationValue += part.Total
} totalPopulation += partPopulation
if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil { if dayN == 0 && multiplicationPercentage == 0 && chickinDate == "" &&
component.TransferID = ref.ID (partDayN > 0 || partMultiplicationPercentage > 0 || partChickinDate != "") {
component.TransferDate = ref.Date dayN = partDayN
component.TransferQty = ref.Qty 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) 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{ rows = append(rows, dto.ExpenseDepreciationV2RowDTO{
Date: dayStr, Date: dayStr,
DepreciationPercentEffective: effectivePercent, DepreciationPercentEffective: effectivePercent,
@@ -519,7 +483,6 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue, TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
StandardEffectiveDate: standardEffectiveDate, StandardEffectiveDate: standardEffectiveDate,
TotalPopulation: totalPopulation, TotalPopulation: totalPopulation,
Components: parseSnapshotComponents(componentsJSON),
}) })
actualDays++ actualDays++
} }