package service import ( "context" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) const ( hppV2ComponentPakan = "PAKAN" hppV2ComponentOvk = "OVK" hppV2ComponentDocChickin = "DOC_CHICKIN" hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE" hppV2ComponentBopRegular = "BOP_REGULAR" hppV2ComponentBopEksp = "BOP_EKSPEDISI" hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST" hppV2ComponentRecordingStockRoute = "RECORDING_STOCK_ROUTE" hppV2ComponentDepreciation = "DEPRECIATION" hppV2PartGrowingNormal = "growing_normal" hppV2PartGrowingCutover = "growing_cutover" hppV2PartLayingNormal = "laying_normal" hppV2PartLayingCutover = "laying_cutover" hppV2PartGrowingDirect = "growing_direct" hppV2PartGrowingFarm = "growing_farm" hppV2PartLayingDirect = "laying_direct" hppV2PartLayingFarm = "laying_farm" hppV2PartManualCutover = "manual_cutover" hppV2PartRecordingStockRoute = "recording_stock_route" hppV2PartDepreciationNormal = "normal_transfer" hppV2PartDepreciationCutover = "manual_cutover" hppV2PartDepreciationFarmSnapshot = "farm_snapshot" hppV2ProrationPopulation = "growing_population_share" hppV2ProrationEggWeight = "laying_egg_weight_share" hppV2ProrationEggPiece = "laying_egg_piece_share" hppV2ScopePulletCost = "pullet_cost" hppV2ScopeProductionCost = "production_cost" hppV2CutoverFlagPakan = "PAKAN-CUTOVER" hppV2CutoverFlagOvk = "OVK" ) type HppV2Service interface { CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error) GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetCostDocChickin(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetCostDirectPulletPurchase(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error) GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetDocChickinBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) } type hppV2Service struct { hppRepo commonRepo.HppV2CostRepository } type hppV2StockComponentConfig struct { Code string Title string NormalFlags []string CutoverFlags []string } type hppV2ExpenseComponentConfig struct { Code string Title string Ekspedisi bool } func NewHppV2Service(hppRepo commonRepo.HppV2CostRepository) HppV2Service { return &hppV2Service{hppRepo: hppRepo} } func (s *hppV2Service) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { breakdown, err := s.CalculateHppBreakdown(projectFlockKandangId, date) if err != nil { return nil, err } if breakdown == nil { return &HppCostResponse{}, nil } result := breakdown.Hpp return &result, nil } func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *time.Time) (*HppV2Breakdown, error) { if s.hppRepo == nil { return &HppV2Breakdown{ ProjectFlockKandangID: projectFlockKandangId, Hpp: HppCostResponse{}, }, nil } startOfDay, endOfDay, err := hppV2DayWindow(date) if err != nil { return nil, err } contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId) if err != nil { return nil, err } pakanComponent, err := s.GetPakanBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } totalPulletCost := 0.0 totalProductionCost := 0.0 components := make([]HppV2Component, 0, 8) appendComponent := func(requestedCode string, component *HppV2Component) { pulletBefore := totalPulletCost productionBefore := totalProductionCost if component == nil || (component.Total == 0 && len(component.Parts) == 0) { utils.Log.Infof( "HPP v2 component skipped: project_flock_kandang_id=%d period_date=%s component=%s reason=empty_or_nil total_pullet_cost=%.2f total_production_cost=%.2f", projectFlockKandangId, startOfDay.Format("2006-01-02"), requestedCode, totalPulletCost, totalProductionCost, ) return } pulletAdded := componentScopeTotal(component, hppV2ScopePulletCost) productionAdded := componentScopeTotal(component, hppV2ScopeProductionCost) components = append(components, *component) totalPulletCost += pulletAdded totalProductionCost += productionAdded utils.Log.Infof( "HPP v2 component applied: project_flock_kandang_id=%d period_date=%s component=%s component_total=%.2f pullet_added=%.2f production_added=%.2f total_pullet_before=%.2f total_pullet_after=%.2f total_production_before=%.2f total_production_after=%.2f parts_count=%d", projectFlockKandangId, startOfDay.Format("2006-01-02"), component.Code, component.Total, pulletAdded, productionAdded, pulletBefore, totalPulletCost, productionBefore, totalProductionCost, len(component.Parts), ) } appendComponent(hppV2ComponentPakan, pakanComponent) ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentOvk, ovkComponent) docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentDocChickin, docComponent) directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentDirectPulletPurchase, directPulletComponent) bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentBopRegular, bopRegularComponent) bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentBopEksp, bopEkspedisiComponent) manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent) recordingStockRouteComponent, err := s.getRecordingStockRouteComponent(projectFlockKandangId, contextRow, startOfDay) if err != nil { return nil, err } appendComponent(hppV2ComponentRecordingStockRoute, recordingStockRouteComponent) depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost) if err != nil { return nil, err } depreciationCostToProduction := componentScopeTotal(depreciationComponent, hppV2ScopeProductionCost) depreciationSource := "" if depreciationComponent != nil && len(depreciationComponent.Parts) > 0 { depreciationSource = depreciationComponent.Parts[0].Code } productionCostBeforeDepreciation := totalProductionCost appendComponent(hppV2ComponentDepreciation, depreciationComponent) utils.Log.Infof( "HPP v2 depreciation cost applied: project_flock_kandang_id=%d period_date=%s depreciation_source=%s depreciation_cost=%.2f production_cost_before=%.2f production_cost_after=%.2f", projectFlockKandangId, startOfDay.Format("2006-01-02"), depreciationSource, depreciationCostToProduction, productionCostBeforeDepreciation, totalProductionCost, ) hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) if err != nil { return nil, err } if hppCost == nil { hppCost = &HppCostResponse{} } return &HppV2Breakdown{ ProjectFlockKandangID: projectFlockKandangId, ProjectFlockID: contextRow.ProjectFlockID, ProjectFlockCategory: contextRow.ProjectFlockCategory, HouseType: contextRow.HouseType, KandangID: contextRow.KandangID, KandangName: contextRow.KandangName, LocationID: contextRow.LocationID, PeriodDate: startOfDay.Format("2006-01-02"), Window: HppV2DateWindow{ Start: startOfDay.Format(time.RFC3339), End: endOfDay.Format(time.RFC3339), }, TotalPulletCost: totalPulletCost, TotalProductionCost: totalProductionCost, Components: components, Hpp: *hppCost, }, nil } func (s *hppV2Service) GetCostPakan(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetPakanBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetPakanBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{ Code: hppV2ComponentPakan, Title: "Pakan", NormalFlags: []string{string(utils.FlagPakan)}, CutoverFlags: []string{hppV2CutoverFlagPakan}, }) } func (s *hppV2Service) GetCostOvk(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetOvkBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetOvkBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { return s.getStockUsageComponent(projectFlockKandangId, endDate, hppV2StockComponentConfig{ Code: hppV2ComponentOvk, Title: "OVK", NormalFlags: []string{ string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), }, CutoverFlags: []string{hppV2CutoverFlagOvk}, }) } func (s *hppV2Service) GetCostDocChickin(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetDocChickinBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetCostDirectPulletPurchase(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetDocChickinBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { if s.hppRepo == nil { return &HppV2Component{ Code: hppV2ComponentDocChickin, Title: "DOC Chick-in", Scopes: []string{hppV2ScopePulletCost}, Parts: []HppV2ComponentPart{}, }, nil } contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId) if err != nil { return nil, err } part, err := s.buildGrowingChickinPart(projectFlockKandangId, contextRow, endDate, []string{string(utils.FlagDOC)}, false, hppV2PartGrowingDirect, "Growing DOC") if err != nil { return nil, err } parts := make([]HppV2ComponentPart, 0, 1) total := 0.0 if part != nil { parts = append(parts, *part) total += part.Total } return &HppV2Component{ Code: hppV2ComponentDocChickin, Title: "DOC Chick-in", Scopes: []string{hppV2ScopePulletCost}, Total: total, Parts: parts, }, nil } func (s *hppV2Service) GetDirectPulletPurchaseBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { part, err := s.buildLayingChickinPart(projectFlockKandangId, endDate, []string{string(utils.FlagPullet), string(utils.FlagLayer)}, true, hppV2PartLayingDirect, "Laying Direct Pullet") if err != nil { return nil, err } parts := make([]HppV2ComponentPart, 0, 1) total := 0.0 if part != nil { parts = append(parts, *part) total += part.Total } return &HppV2Component{ Code: hppV2ComponentDirectPulletPurchase, Title: "Direct Pullet Purchase", Scopes: []string{hppV2ScopeProductionCost}, Total: total, Parts: parts, }, nil } func (s *hppV2Service) GetCostBopRegular(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetBopRegularBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetCostBopEkspedisi(projectFlockKandangId uint, endDate *time.Time) (float64, error) { component, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, endDate) if err != nil { return 0, err } if component == nil { return 0, nil } return component.Total, nil } func (s *hppV2Service) GetBopRegularBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{ Code: hppV2ComponentBopRegular, Title: "BOP Regular", Ekspedisi: false, }) } func (s *hppV2Service) GetBopEkspedisiBreakdown(projectFlockKandangId uint, endDate *time.Time) (*HppV2Component, error) { return s.getExpenseComponent(projectFlockKandangId, endDate, hppV2ExpenseComponentConfig{ Code: hppV2ComponentBopEksp, Title: "BOP Ekspedisi", Ekspedisi: true, }) } func (s *hppV2Service) getStockUsageComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig) (*HppV2Component, error) { if s.hppRepo == nil { return &HppV2Component{ Code: config.Code, Title: config.Title, Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost}, Parts: []HppV2ComponentPart{}, }, nil } contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId) if err != nil { return nil, err } parts := make([]HppV2ComponentPart, 0, 4) total := 0.0 growingPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, false) if err != nil { return nil, err } if growingPart != nil { parts = append(parts, *growingPart) total += growingPart.Total } growingCutoverPart, err := s.buildGrowingUsagePart(projectFlockKandangId, contextRow, endDate, config, true) if err != nil { return nil, err } if growingCutoverPart != nil { parts = append(parts, *growingCutoverPart) total += growingCutoverPart.Total } layingNormalPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, false) if err != nil { return nil, err } if layingNormalPart != nil { parts = append(parts, *layingNormalPart) total += layingNormalPart.Total } layingCutoverPart, err := s.buildLayingUsagePart(projectFlockKandangId, endDate, config, true) if err != nil { return nil, err } if layingCutoverPart != nil { parts = append(parts, *layingCutoverPart) total += layingCutoverPart.Total } return &HppV2Component{ Code: config.Code, Title: config.Title, Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost}, Total: total, Parts: parts, }, nil } func (s *hppV2Service) getExpenseComponent(projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig) (*HppV2Component, error) { if s.hppRepo == nil { return &HppV2Component{ Code: config.Code, Title: config.Title, Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost}, Parts: []HppV2ComponentPart{}, }, nil } contextRow, err := s.hppRepo.GetProjectFlockKandangContext(context.Background(), projectFlockKandangId) if err != nil { return nil, err } parts := make([]HppV2ComponentPart, 0, 4) total := 0.0 growingDirect, err := s.buildGrowingExpenseDirectPart(projectFlockKandangId, contextRow, endDate, config) if err != nil { return nil, err } if growingDirect != nil { parts = append(parts, *growingDirect) total += growingDirect.Total } growingFarm, err := s.buildGrowingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config) if err != nil { return nil, err } if growingFarm != nil { parts = append(parts, *growingFarm) total += growingFarm.Total } layingDirect, err := s.buildLayingExpenseDirectPart(projectFlockKandangId, endDate, config) if err != nil { return nil, err } if layingDirect != nil { parts = append(parts, *layingDirect) total += layingDirect.Total } layingFarm, err := s.buildLayingExpenseFarmPart(projectFlockKandangId, contextRow, endDate, config) if err != nil { return nil, err } if layingFarm != nil { parts = append(parts, *layingFarm) total += layingFarm.Total } return &HppV2Component{ Code: config.Code, Title: config.Title, Scopes: []string{hppV2ScopePulletCost, hppV2ScopeProductionCost}, Total: total, Parts: parts, }, nil } func (s *hppV2Service) buildGrowingChickinPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, flagNames []string, excludeTransferToLaying bool, partCode string, partTitle string, ) (*HppV2ComponentPart, error) { if contextRow == nil { return nil, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { return nil, err } if sourceProjectFlockID == 0 || transferTotalQty <= 0 { return nil, nil } kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { return nil, err } if len(kandangIDsGrowing) == 0 { return nil, nil } totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) if err != nil { return nil, err } if totalPopulationFlockGrowing <= 0 { return nil, nil } ratio := transferTotalQty / totalPopulationFlockGrowing if ratio <= 0 { return nil, nil } rows, err := s.hppRepo.ListChickinCostRowsByProductFlags(context.Background(), kandangIDsGrowing, flagNames, endDate, excludeTransferToLaying) if err != nil { return nil, err } return buildChickinPartFromRows( rows, partCode, partTitle, []string{hppV2ScopePulletCost}, &HppV2Proration{ Basis: hppV2ProrationPopulation, Numerator: transferTotalQty, Denominator: totalPopulationFlockGrowing, Ratio: ratio, }, ratio, ), nil } func (s *hppV2Service) buildLayingChickinPart( projectFlockKandangId uint, endDate *time.Time, flagNames []string, excludeTransferToLaying bool, partCode string, partTitle string, ) (*HppV2ComponentPart, error) { rows, err := s.hppRepo.ListChickinCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, flagNames, endDate, excludeTransferToLaying) if err != nil { return nil, err } return buildChickinPartFromRows(rows, partCode, partTitle, []string{hppV2ScopeProductionCost}, nil, 1), nil } func (s *hppV2Service) buildGrowingUsagePart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, config hppV2StockComponentConfig, cutover bool, ) (*HppV2ComponentPart, error) { if contextRow == nil { return nil, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { return nil, err } if sourceProjectFlockID == 0 || transferTotalQty <= 0 { return nil, nil } kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { return nil, err } if len(kandangIDsGrowing) == 0 { return nil, nil } totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) if err != nil { return nil, err } if totalPopulationFlockGrowing == 0 { return nil, nil } ratio := transferTotalQty / totalPopulationFlockGrowing if ratio <= 0 { return nil, nil } partCode := hppV2PartGrowingNormal partTitle := "Growing" baseRows := make([]HppV2Reference, 0) baseTotal := 0.0 if cutover { partCode = hppV2PartGrowingCutover partTitle = "Growing Cut-over" rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.CutoverFlags, endDate) if err != nil { return nil, err } for _, row := range rows { rowTotal := adjustmentRowTotalCost(row) baseTotal += rowTotal baseRows = append(baseRows, HppV2Reference{ Type: "adjustment_stock", ID: row.AdjustmentID, ProjectFlockKandangID: row.ProjectFlockKandangID, ProductID: row.ProductID, ProductName: row.ProductName, Date: row.CreatedAt.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.Price, Total: rowTotal, AppliedTotal: rowTotal * ratio, }) } } else { rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), kandangIDsGrowing, config.NormalFlags, endDate) if err != nil { return nil, err } for _, row := range rows { baseTotal += row.TotalCost refDate := row.LastUsedAt if refDate.IsZero() { refDate = row.FirstUsedAt } baseRows = append(baseRows, HppV2Reference{ Type: "stock_allocation", ID: row.StockableID, StockableType: row.StockableType, ProductID: row.SourceProductID, ProductName: row.SourceProductName, Date: refDate.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.UnitPrice, Total: row.TotalCost, AppliedTotal: row.TotalCost * ratio, }) } } if baseTotal == 0 { return nil, nil } return &HppV2ComponentPart{ Code: partCode, Title: partTitle, Scopes: []string{hppV2ScopePulletCost}, Total: baseTotal * ratio, Proration: &HppV2Proration{ Basis: hppV2ProrationPopulation, Numerator: transferTotalQty, Denominator: totalPopulationFlockGrowing, Ratio: ratio, }, References: baseRows, }, nil } func (s *hppV2Service) buildLayingUsagePart( projectFlockKandangId uint, endDate *time.Time, config hppV2StockComponentConfig, cutover bool, ) (*HppV2ComponentPart, error) { if cutover { rows, err := s.hppRepo.ListAdjustmentCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.CutoverFlags, endDate) if err != nil { return nil, err } total := 0.0 references := make([]HppV2Reference, 0, len(rows)) for _, row := range rows { rowTotal := adjustmentRowTotalCost(row) total += rowTotal references = append(references, HppV2Reference{ Type: "adjustment_stock", ID: row.AdjustmentID, ProjectFlockKandangID: row.ProjectFlockKandangID, ProductID: row.ProductID, ProductName: row.ProductName, Date: row.CreatedAt.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.Price, Total: rowTotal, AppliedTotal: rowTotal, }) } if total == 0 { return nil, nil } return &HppV2ComponentPart{ Code: hppV2PartLayingCutover, Title: "Laying Cut-over", Scopes: []string{hppV2ScopeProductionCost}, Total: total, References: references, }, nil } rows, err := s.hppRepo.ListUsageCostRowsByProductFlags(context.Background(), []uint{projectFlockKandangId}, config.NormalFlags, endDate) if err != nil { return nil, err } total := 0.0 references := make([]HppV2Reference, 0, len(rows)) for _, row := range rows { total += row.TotalCost refDate := row.LastUsedAt if refDate.IsZero() { refDate = row.FirstUsedAt } references = append(references, HppV2Reference{ Type: "stock_allocation", ID: row.StockableID, StockableType: row.StockableType, ProductID: row.SourceProductID, ProductName: row.SourceProductName, Date: refDate.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.UnitPrice, Total: row.TotalCost, AppliedTotal: row.TotalCost, }) } if total == 0 { return nil, nil } return &HppV2ComponentPart{ Code: hppV2PartLayingNormal, Title: "Laying", Scopes: []string{hppV2ScopeProductionCost}, Total: total, References: references, }, nil } func (s *hppV2Service) buildGrowingExpenseDirectPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, config hppV2ExpenseComponentConfig, ) (*HppV2ComponentPart, error) { return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, false) } func (s *hppV2Service) buildGrowingExpenseFarmPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, config hppV2ExpenseComponentConfig, ) (*HppV2ComponentPart, error) { return s.buildGrowingExpensePart(projectFlockKandangId, contextRow, endDate, config, true) } func (s *hppV2Service) buildGrowingExpensePart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, config hppV2ExpenseComponentConfig, farmLevel bool, ) (*HppV2ComponentPart, error) { if contextRow == nil { return nil, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { return nil, err } if sourceProjectFlockID == 0 || transferTotalQty <= 0 { return nil, nil } kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { return nil, err } if len(kandangIDsGrowing) == 0 { return nil, nil } totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) if err != nil { return nil, err } if totalPopulationFlockGrowing <= 0 { return nil, nil } ratio := transferTotalQty / totalPopulationFlockGrowing if ratio <= 0 { return nil, nil } var rows []commonRepo.HppV2ExpenseCostRow if farmLevel { rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), sourceProjectFlockID, endDate, config.Ekspedisi) } else { rows, err = s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), kandangIDsGrowing, endDate, config.Ekspedisi) } if err != nil { return nil, err } return buildExpensePartFromRows( rows, map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel], map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel], []string{hppV2ScopePulletCost}, &HppV2Proration{ Basis: hppV2ProrationPopulation, Numerator: transferTotalQty, Denominator: totalPopulationFlockGrowing, Ratio: ratio, }, ratio, ), nil } func (s *hppV2Service) buildLayingExpenseDirectPart( projectFlockKandangId uint, endDate *time.Time, config hppV2ExpenseComponentConfig, ) (*HppV2ComponentPart, error) { rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockKandangIDs(context.Background(), []uint{projectFlockKandangId}, endDate, config.Ekspedisi) if err != nil { return nil, err } return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", []string{hppV2ScopeProductionCost}, nil, 1), nil } func (s *hppV2Service) buildLayingExpenseFarmPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, endDate *time.Time, config hppV2ExpenseComponentConfig, ) (*HppV2ComponentPart, error) { if contextRow == nil { return nil, nil } rows, err := s.hppRepo.ListExpenseRealizationRowsByProjectFlockID(context.Background(), contextRow.ProjectFlockID, endDate, config.Ekspedisi) if err != nil { return nil, err } if len(rows) == 0 { return nil, nil } farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return nil, err } farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, endDate) if err != nil { return nil, err } basis := hppV2ProrationEggWeight numerator := targetWeight denominator := farmWeight if denominator <= 0 { basis = hppV2ProrationEggPiece numerator = targetPieces denominator = farmPieces } if denominator <= 0 { return nil, nil } ratio := numerator / denominator if ratio <= 0 { return nil, nil } return buildExpensePartFromRows( rows, hppV2PartLayingFarm, "Laying Farm", []string{hppV2ScopeProductionCost}, &HppV2Proration{ Basis: basis, Numerator: numerator, Denominator: denominator, Ratio: ratio, }, ratio, ), nil } func (s *hppV2Service) getManualPulletCostComponent( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, ) (*HppV2Component, error) { if s.hppRepo == nil || contextRow == nil { return nil, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { return nil, err } if sourceProjectFlockID != 0 && transferTotalQty > 0 { return nil, nil } manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() { return nil, nil } if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) { return nil, nil } farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if len(farmPFKIDs) == 0 { return nil, nil } totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs) if err != nil { return nil, err } if totalPopulation <= 0 { return nil, nil } targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId}) if err != nil { return nil, err } if targetPopulation <= 0 { return nil, nil } ratio := targetPopulation / totalPopulation if ratio <= 0 { return nil, nil } appliedTotal := manualInput.TotalCost * ratio part := HppV2ComponentPart{ Code: hppV2PartManualCutover, Title: "Manual Cut-over", Scopes: []string{hppV2ScopePulletCost}, Total: appliedTotal, Proration: &HppV2Proration{ Basis: hppV2ProrationPopulation, Numerator: targetPopulation, Denominator: totalPopulation, Ratio: ratio, }, Details: map[string]any{ "cutover_date": formatDateOnly(manualInput.CutoverDate), "farm_total_cost": manualInput.TotalCost, "target_population": targetPopulation, "farm_population": totalPopulation, }, References: []HppV2Reference{ { Type: "farm_depreciation_manual_input", ID: manualInput.ID, Date: formatDateOnly(manualInput.CutoverDate), Qty: 1, Total: manualInput.TotalCost, AppliedTotal: appliedTotal, }, }, } return &HppV2Component{ Code: hppV2ComponentManualPulletCost, Title: "Manual Pullet Cost", Scopes: []string{hppV2ScopePulletCost}, Total: appliedTotal, Parts: []HppV2ComponentPart{part}, }, nil } func (s *hppV2Service) getRecordingStockRouteComponent( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, ) (*HppV2Component, error) { if s.hppRepo == nil || contextRow == nil || periodDate.IsZero() { return nil, nil } farmTotalCost, err := s.hppRepo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID( context.Background(), contextRow.ProjectFlockID, periodDate, ) if err != nil { return nil, err } if farmTotalCost <= 0 { return nil, nil } farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if len(farmPFKIDs) == 0 { return nil, nil } totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs) if err != nil { return nil, err } if totalPopulation <= 0 { return nil, nil } targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId}) if err != nil { return nil, err } if targetPopulation <= 0 { return nil, nil } ratio := targetPopulation / totalPopulation if ratio <= 0 { return nil, nil } appliedTotal := farmTotalCost * ratio if appliedTotal <= 0 { return nil, nil } part := HppV2ComponentPart{ Code: hppV2PartRecordingStockRoute, Title: "Recording Stock Route", Scopes: []string{hppV2ScopePulletCost}, Total: appliedTotal, Proration: &HppV2Proration{ Basis: hppV2ProrationPopulation, Numerator: targetPopulation, Denominator: totalPopulation, Ratio: ratio, }, Details: map[string]any{ "period_date": formatDateOnly(periodDate), "farm_total_cost": farmTotalCost, "target_population": targetPopulation, "farm_population": totalPopulation, "project_flock_id": contextRow.ProjectFlockID, "project_flock_kandang_id": projectFlockKandangId, }, References: []HppV2Reference{ { Type: "recording_stock_route", Date: formatDateOnly(periodDate), Qty: 1, Total: farmTotalCost, AppliedTotal: appliedTotal, }, }, } return &HppV2Component{ Code: hppV2ComponentRecordingStockRoute, Title: "Recording Stock Route", Scopes: []string{hppV2ScopePulletCost}, Total: appliedTotal, Parts: []HppV2ComponentPart{part}, }, nil } func (s *hppV2Service) getDepreciationComponent( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, endDate time.Time, totalPulletCost float64, ) (*HppV2Component, error) { if s.hppRepo == nil || contextRow == nil { return nil, nil } snapshotPart, err := s.buildFarmSnapshotDepreciationPart(projectFlockKandangId, contextRow, periodDate, endDate) if err != nil { return nil, err } if snapshotPart != nil { return &HppV2Component{ Code: hppV2ComponentDepreciation, Title: "Depreciation", Scopes: []string{hppV2ScopeProductionCost}, Total: snapshotPart.Total, Parts: []HppV2ComponentPart{*snapshotPart}, }, nil } if totalPulletCost <= 0 { return nil, nil } transferInput, err := s.hppRepo.GetLatestTransferInputByProjectFlockKandangID(context.Background(), projectFlockKandangId, periodDate) if err != nil { return nil, err } var part *HppV2ComponentPart if transferInput != nil && transferInput.SourceProjectFlockID > 0 { part, err = s.buildNormalTransferDepreciationPart(contextRow, transferInput, periodDate, totalPulletCost) if err != nil { return nil, err } } else { part, err = s.buildManualCutoverDepreciationPart(projectFlockKandangId, contextRow, periodDate, totalPulletCost) if err != nil { return nil, err } } if part == nil { return nil, nil } return &HppV2Component{ Code: hppV2ComponentDepreciation, Title: "Depreciation", Scopes: []string{hppV2ScopeProductionCost}, Total: part.Total, Parts: []HppV2ComponentPart{*part}, }, nil } func (s *hppV2Service) buildFarmSnapshotDepreciationPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, endDate time.Time, ) (*HppV2ComponentPart, error) { if contextRow == nil { return nil, nil } snapshot, err := s.hppRepo.GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(context.Background(), contextRow.ProjectFlockID, periodDate) if err != nil { return nil, err } if snapshot == nil || snapshot.DepreciationValue <= 0 { return nil, nil } farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if len(farmPFKIDs) == 0 { return nil, nil } end := endDate targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, &end) if err != nil { return nil, err } farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, &end) if err != nil { return nil, err } basis := hppV2ProrationEggWeight numerator := targetWeight denominator := farmWeight if denominator <= 0 { basis = hppV2ProrationEggPiece numerator = targetPieces denominator = farmPieces } if denominator <= 0 { return nil, nil } ratio := numerator / denominator if ratio <= 0 { return nil, nil } appliedDepreciation := snapshot.DepreciationValue * ratio if appliedDepreciation <= 0 { return nil, nil } appliedPulletCostDayN := snapshot.PulletCostDayNTotal * ratio depreciationPercent := snapshot.DepreciationPercentEffective if appliedPulletCostDayN > 0 { depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100 } return &HppV2ComponentPart{ Code: hppV2PartDepreciationFarmSnapshot, Title: "Farm Snapshot", Scopes: []string{hppV2ScopeProductionCost}, Total: appliedDepreciation, Proration: &HppV2Proration{ Basis: basis, Numerator: numerator, 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, }, References: []HppV2Reference{ { Type: "farm_depreciation_snapshot", ID: snapshot.ID, Date: formatDateOnly(snapshot.PeriodDate), Qty: 1, Total: snapshot.DepreciationValue, AppliedTotal: appliedDepreciation, }, }, }, nil } func (s *hppV2Service) buildNormalTransferDepreciationPart( contextRow *commonRepo.HppV2ProjectFlockKandangContext, transferInput *commonRepo.HppV2LatestTransferInputRow, periodDate time.Time, totalPulletCost float64, ) (*HppV2ComponentPart, error) { if contextRow == nil || transferInput == nil || totalPulletCost <= 0 { return nil, nil } originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), transferInput.SourceProjectFlockID) if err != nil { return nil, err } if originDate == nil || originDate.IsZero() { return nil, nil } scheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType) if scheduleDay <= 0 { return nil, nil } houseType := NormalizeDepreciationHouseType(contextRow.HouseType) percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, scheduleDay) if err != nil { return nil, err } pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationAtDayN( totalPulletCost, scheduleDay, contextRow.HouseType, percentByHouseType, ) if depreciationValue <= 0 { return nil, nil } return &HppV2ComponentPart{ Code: hppV2PartDepreciationNormal, Title: "Normal Transfer", Scopes: []string{hppV2ScopeProductionCost}, Total: depreciationValue, Details: map[string]any{ "basis_total": totalPulletCost, "pullet_cost_day_n": pulletCostDayN, "depreciation_percent": depreciationPercent, "schedule_day": scheduleDay, "origin_date": formatDateOnly(*originDate), "transfer_date": formatDateOnly(transferInput.TransferDate), "source_project_flock_id": transferInput.SourceProjectFlockID, }, References: []HppV2Reference{ { Type: "laying_transfer", ID: transferInput.TransferID, Date: formatDateOnly(transferInput.TransferDate), Qty: transferInput.TransferQty, Total: totalPulletCost, AppliedTotal: depreciationValue, }, }, }, nil } func (s *hppV2Service) buildManualCutoverDepreciationPart( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, totalPulletCost float64, ) (*HppV2ComponentPart, error) { if contextRow == nil || totalPulletCost <= 0 { return nil, nil } manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() { return nil, nil } if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) { return nil, nil } originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), contextRow.ProjectFlockID) if err != nil { return nil, err } if originDate == nil || originDate.IsZero() { return nil, nil } reportScheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType) if reportScheduleDay <= 0 { return nil, nil } cutoverScheduleDay := DepreciationScheduleDay(*originDate, manualInput.CutoverDate, contextRow.HouseType) startDay := 1 if cutoverScheduleDay > 0 { startDay = cutoverScheduleDay } houseType := NormalizeDepreciationHouseType(contextRow.HouseType) percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, reportScheduleDay) if err != nil { return nil, err } pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange( totalPulletCost, startDay, reportScheduleDay, contextRow.HouseType, percentByHouseType, ) if depreciationValue <= 0 { return nil, nil } return &HppV2ComponentPart{ Code: hppV2PartDepreciationCutover, Title: "Manual Cut-over", Scopes: []string{hppV2ScopeProductionCost}, Total: depreciationValue, Details: map[string]any{ "basis_total": totalPulletCost, "pullet_cost_day_n": pulletCostDayN, "depreciation_percent": depreciationPercent, "schedule_day": reportScheduleDay, "start_schedule_day": startDay, "origin_date": formatDateOnly(*originDate), "cutover_date": formatDateOnly(manualInput.CutoverDate), "manual_input_id": manualInput.ID, "project_flock_kandang": projectFlockKandangId, }, References: []HppV2Reference{ { Type: "farm_depreciation_manual_input", ID: manualInput.ID, Date: formatDateOnly(manualInput.CutoverDate), Qty: 1, Total: totalPulletCost, AppliedTotal: depreciationValue, }, }, }, nil } func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { utils.Log.Infof( "GetHppEstimationDanRealisasi started: project_flock_kandang_id=%d total_production_cost=%.2f start_date=%s end_date=%s", projectFlockKandangId, totalProductionCost, formatTimePtr(startDate), formatTimePtr(endDate), ) if s.hppRepo == nil { utils.Log.Warnf( "GetHppEstimationDanRealisasi skipped: hpp repository is nil (project_flock_kandang_id=%d)", projectFlockKandangId, ) return &HppCostResponse{}, nil } estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { utils.Log.WithError(err).Errorf( "GetHppEstimationDanRealisasi failed to get estimation egg production: project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate), ) return nil, err } realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { utils.Log.WithError(err).Errorf( "GetHppEstimationDanRealisasi failed to get realization egg sales: project_flock_kandang_id=%d start_date=%s end_date=%s", projectFlockKandangId, formatTimePtr(startDate), formatTimePtr(endDate), ) return nil, err } estimation := HppCostDetail{ Total: totalProductionCost, Kg: estimWeightKg, Butir: estimPieces, } if estimWeightKg > 0 { estimation.HargaKg = roundToTwoDecimals(totalProductionCost / estimWeightKg) } if estimPieces > 0 { estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces) } real := HppCostDetail{ Total: totalProductionCost, Kg: realWeightKg, Butir: realPieces, } if realWeightKg > 0 { real.HargaKg = roundToTwoDecimals(totalProductionCost / realWeightKg) } if realPieces > 0 { real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) } utils.Log.Infof( "GetHppEstimationDanRealisasi success: project_flock_kandang_id=%d estimation_butir=%.2f estimation_kg=%.2f estimation_harga_butir=%.2f estimation_harga_kg=%.2f real_butir=%.2f real_kg=%.2f real_harga_butir=%.2f real_harga_kg=%.2f totalProductionCost=%.2f", projectFlockKandangId, estimation.Butir, estimation.Kg, estimation.HargaButir, estimation.HargaKg, real.Butir, real.Kg, real.HargaButir, real.HargaKg, totalProductionCost, ) return &HppCostResponse{ Estimation: estimation, Real: real, }, nil } func hppV2DayWindow(date *time.Time) (time.Time, time.Time, error) { if date == nil { now := time.Now() date = &now } location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return time.Time{}, time.Time{}, err } startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) endOfDay := startOfDay.Add(24 * time.Hour) return startOfDay, endOfDay, nil } func adjustmentRowTotalCost(row commonRepo.HppV2AdjustmentCostRow) float64 { if row.GrandTotal > 0 { return row.GrandTotal } return row.Qty * row.Price } func buildExpensePartFromRows( rows []commonRepo.HppV2ExpenseCostRow, code string, title string, scopes []string, proration *HppV2Proration, ratio float64, ) *HppV2ComponentPart { if len(rows) == 0 { return nil } total := 0.0 references := make([]HppV2Reference, 0, len(rows)) for _, row := range rows { total += row.TotalCost * ratio references = append(references, HppV2Reference{ Type: "expense_realization", ID: row.ExpenseRealizationID, ProductID: row.NonstockID, ProductName: row.NonstockName, Date: row.RealizationDate.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.Price, Total: row.TotalCost, AppliedTotal: row.TotalCost * ratio, }) } if total == 0 { return nil } return &HppV2ComponentPart{ Code: code, Title: title, Scopes: append([]string{}, scopes...), Total: total, Proration: proration, References: references, } } func buildChickinPartFromRows( rows []commonRepo.HppV2ChickinCostRow, code string, title string, scopes []string, proration *HppV2Proration, ratio float64, ) *HppV2ComponentPart { if len(rows) == 0 { return nil } total := 0.0 references := make([]HppV2Reference, 0, len(rows)) for _, row := range rows { total += row.TotalCost * ratio projectFlockKandangID := row.ProjectFlockKandangID references = append(references, HppV2Reference{ Type: "project_chickin", ID: row.ProjectChickinID, StockableType: row.StockableType, ProjectFlockKandangID: &projectFlockKandangID, ProductID: row.SourceProductID, ProductName: row.SourceProductName, Date: row.ChickInDate.Format("2006-01-02"), Qty: row.Qty, UnitPrice: row.UnitPrice, Total: row.TotalCost, AppliedTotal: row.TotalCost * ratio, }) } if total == 0 { return nil } return &HppV2ComponentPart{ Code: code, Title: title, Scopes: append([]string{}, scopes...), Total: total, Proration: proration, References: references, } } func componentHasScope(component *HppV2Component, scope string) bool { if component == nil || scope == "" { return false } for _, candidate := range component.Scopes { if candidate == scope { return true } } return false } func componentScopeTotal(component *HppV2Component, scope string) float64 { if component == nil || scope == "" { return 0 } total := 0.0 hasPartScopes := false for _, part := range component.Parts { if len(part.Scopes) == 0 { continue } hasPartScopes = true if partHasScope(&part, scope) { total += part.Total } } if hasPartScopes { return total } if componentHasScope(component, scope) { return component.Total } return 0 } func partHasScope(part *HppV2ComponentPart, scope string) bool { if part == nil || scope == "" { return false } for _, candidate := range part.Scopes { if candidate == scope { return true } } return false } func dateOnly(value time.Time) time.Time { return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location()) } func formatDateOnly(value time.Time) string { return dateOnly(value).Format("2006-01-02") }