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" hppV2ComponentBopRegular = "BOP_REGULAR" hppV2ComponentBopEksp = "BOP_EKSPEDISI" hppV2PartGrowingNormal = "growing_normal" hppV2PartGrowingCutover = "growing_cutover" hppV2PartLayingNormal = "laying_normal" hppV2PartLayingCutover = "laying_cutover" hppV2PartGrowingDirect = "growing_direct" hppV2PartGrowingFarm = "growing_farm" hppV2PartLayingDirect = "laying_direct" hppV2PartLayingFarm = "laying_farm" hppV2ProrationPopulation = "growing_population_share" hppV2ProrationEggWeight = "laying_egg_weight_share" hppV2ProrationEggPiece = "laying_egg_piece_share" hppV2CutoverFlagPakan = "PAKAN-CUTOVER" hppV2CutoverFlagOvk = "OVK-CUTOVER" ) 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) 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) 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 } totalProductionCost := 0.0 components := make([]HppV2Component, 0, 4) if pakanComponent != nil && (pakanComponent.Total > 0 || len(pakanComponent.Parts) > 0) { totalProductionCost += pakanComponent.Total components = append(components, *pakanComponent) } ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } if ovkComponent != nil && (ovkComponent.Total > 0 || len(ovkComponent.Parts) > 0) { totalProductionCost += ovkComponent.Total components = append(components, *ovkComponent) } bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } if bopRegularComponent != nil && (bopRegularComponent.Total > 0 || len(bopRegularComponent.Parts) > 0) { totalProductionCost += bopRegularComponent.Total components = append(components, *bopRegularComponent) } bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } if bopEkspedisiComponent != nil && (bopEkspedisiComponent.Total > 0 || len(bopEkspedisiComponent.Parts) > 0) { totalProductionCost += bopEkspedisiComponent.Total components = append(components, *bopEkspedisiComponent) } 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, 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), }, 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) 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, 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, 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, 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, Total: total, Parts: parts, }, 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, 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", 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", 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], &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", 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", &HppV2Proration{ Basis: basis, Numerator: numerator, Denominator: denominator, Ratio: ratio, }, ratio, ), nil } func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { if s.hppRepo == nil { return &HppCostResponse{}, nil } estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { return nil, err } realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { 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) } 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, 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, Total: total, Proration: proration, References: references, } }