package dto import ( "slices" "strings" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === CONSTANTS === const ( HPPGroupPengeluaran = "HPP dan Pengeluaran" HPPGroupBahanBaku = "HPP dan Bahan Baku" HPPLabelOverhead = "Pengeluaran Overhead" HPPLabelEkspedisi = "Beban Ekspedisi" HPPSummaryLabel = "HPP" PLSalesTypeChicken = "Penjualan Ayam Besar" PLSalesTypeEgg = "Penjualan Telur" PLItemTypeSapronak = "Pembelian Sapronak" PLItemTypeOverhead = "Pengeluaran Overhead" PLItemTypeEkspedisi = "Beban Ekspedisi" PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO" PLSummaryLabelSubTotal = "SUB TOTAL" PLSummaryLabelNetProfit = "LABA RUGI NETTO" PurchaseLabelPrefix = "Pembelian " ) // === CONTEXT STRUCTS === type CalculationContext struct { TotalPopulation float64 TotalWeightProduced float64 TotalEggWeightKg float64 TotalDepletion float64 TotalWeightSold float64 ActualPopulation float64 } type ClosingKeuanganInput struct { ProjectFlockCategory string PurchaseItems []entities.PurchaseItem Budgets []entities.ProjectBudget Realizations []entities.ExpenseRealization DeliveryProducts []entities.MarketingDeliveryProduct Chickins []entities.ProjectChickin TotalWeightProduced float64 TotalEggWeightKg float64 TotalDepletion float64 } // === BASE METRICS === type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` Amount float64 `json:"amount"` } type Comparison struct { Budgeting FinancialMetrics `json:"budgeting"` Realization FinancialMetrics `json:"realization"` } // === HPP PURCHASES PACKAGE === type HppItem struct { Type string `json:"type"` Comparison } type HppGroup struct { GroupName string `json:"group_name"` Data []HppItem `json:"data"` } type SummaryHpp struct { Label string `json:"label"` Budgeting FinancialMetrics `json:"budgeting"` Realization FinancialMetrics `json:"realization"` EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"` EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` } type HppPurchasesSection struct { Hpp []HppGroup `json:"hpp"` SummaryHpp SummaryHpp `json:"summary_hpp"` } // === PROFIT LOSS PACKAGE === type PLItem struct { Type string `json:"type"` FinancialMetrics } type PLSummaryItem struct { Label string `json:"label"` FinancialMetrics } type PLSummaryGroup struct { GrossProfit PLSummaryItem `json:"gross_profit"` SubTotal PLSummaryItem `json:"sub_total"` NetProfit PLSummaryItem `json:"net_profit"` } type ProfitLossData struct { Penjualan []PLItem `json:"penjualan"` Pembelian []PLItem `json:"pembelian"` Overhead PLItem `json:"overhead"` Ekspedisi PLItem `json:"ekspedisi"` Summary PLSummaryGroup `json:"summary"` } type ProfitLossSection struct { Data ProfitLossData `json:"data"` } // === RESPONSE DTO (ROOT) === type ReportResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"` ProfitLoss ProfitLossSection `json:"profit_loss"` } // === MAPPER FUNCTIONS === func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { return FinancialMetrics{ RpPerBird: rpPerBird, RpPerKg: rpPerKg, Amount: amount, } } func ToComparison(budgeting, realization FinancialMetrics) Comparison { return Comparison{ Budgeting: budgeting, Realization: realization, } } // === HPP PENGELUARAN (from Purchase Items) === func getFlagLabel(flagType utils.FlagType) string { return PurchaseLabelPrefix + string(flagType) } func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem { flags := []utils.FlagType{ utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia, } items := []HppItem{} seenFlags := make(map[utils.FlagType]bool) for _, item := range purchaseItems { if item.Product == nil || len(item.Product.Flags) == 0 { continue } for _, flag := range item.Product.Flags { flagType := utils.FlagType(flag.Name) if slices.Contains(flags, flagType) && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced) items = append(items, HppItem{ Type: getFlagLabel(flagType), Comparison: ToComparison( ToFinancialMetrics(rpPerBird, rpPerKg, amount), ToFinancialMetrics(rpPerBird, rpPerKg, amount), ), }) seenFlags[flagType] = true } } } return items } // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem { budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) return HppItem{ Type: HPPLabelOverhead, Comparison: ToComparison( ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), ), } } func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem { ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) return HppItem{ Type: HPPLabelEkspedisi, Comparison: ToComparison( ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), ), } } func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup { items := []HppItem{} budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) realizationAmount := getOperationalExpenses(realizations) if budgetAmount > 0 || realizationAmount > 0 { items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx)) } ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx)) return HppGroup{ GroupName: HPPGroupBahanBaku, Data: items, } } // === HPP SUMMARY === func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp { purchaseTotal := sumPurchaseTotal(purchaseItems) budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) totalBudget := purchaseTotal + budgetTotal totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced) realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) summary := SummaryHpp{ Label: label, Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), } if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 { budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg) realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg) summary.EggBudgeting = &FinancialMetrics{ RpPerBird: 0, RpPerKg: budgetEggRpPerKg, Amount: totalBudget, } summary.EggRealization = &FinancialMetrics{ RpPerBird: 0, RpPerKg: realizationEggRpPerKg, Amount: totalRealization, } } return summary } func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection { hppGroups := []HppGroup{ { GroupName: HPPGroupPengeluaran, Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx), }, ToHppBahanBakuGroup(budgets, realizations, ctx), } summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx) return HppPurchasesSection{ Hpp: hppGroups, SummaryHpp: summaryHpp, } } // === PROFIT & LOSS === func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { return PLItem{ Type: itemType, FinancialMetrics: metrics, } } func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { return PLSummaryItem{ Label: label, FinancialMetrics: metrics, } } func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem { rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced) return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) } func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { for _, item := range items { totalAmount += item.Amount totalPerBird += item.RpPerBird } return } func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem { rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold) return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) } func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem { items := []PLItem{} categorized := categorizeDeliveriesBySalesType(deliveryProducts) if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg]) items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx)) } else { ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) } return items } func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { purchaseAmount := sumPurchaseTotal(purchases) return []PLItem{ createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx), } } func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { realizationAmount := getOperationalExpenses(realizations) return []PLItem{ createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx), } } func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) return []PLItem{ createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx), } } func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems) totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems) grossProfit := totalPenjualan - totalPembelian grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird totalOtherExpenses := totalOverhead + totalEkspedisi totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird netProfit := grossProfit - totalOtherExpenses netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird return PLSummaryGroup{ GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)), } } func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead) totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi) return ProfitLossData{ Penjualan: penjualanItems, Pembelian: pembelianItems, Overhead: totalOverhead, Ekspedisi: totalEkspedisi, Summary: summary, } } func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection { return ProfitLossSection{ Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems), } } func aggregatePLItems(items []PLItem, label string) PLItem { totalAmount, totalPerBird := sumPLItems(items) return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount)) } func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { return ReportResponse{ HppPurchases: hppPurchases, ProfitLoss: profitLoss, } } func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse { var totalPopulation float64 var totalWeightSold float64 for _, chickin := range input.Chickins { totalPopulation += chickin.UsageQty } for _, delivery := range input.DeliveryProducts { totalWeightSold += delivery.TotalWeight } ctx := CalculationContext{ TotalPopulation: totalPopulation, TotalWeightProduced: input.TotalWeightProduced, TotalEggWeightKg: input.TotalEggWeightKg, TotalDepletion: input.TotalDepletion, TotalWeightSold: totalWeightSold, ActualPopulation: totalPopulation - input.TotalDepletion, } hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx) penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx) pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx) overheadItems := ToOverheadItems(input.Realizations, ctx) ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) } // === HELPER FUNCTIONS === func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) { if totalPopulation > 0 { rpPerBird = amount / totalPopulation } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } return rpPerBird, rpPerKg } func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool { for _, flag := range flags { if strings.ToUpper(flag.Name) == string(flagType) { return true } } return false } func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { return func(item *entities.PurchaseItem) bool { if item.Product == nil || len(item.Product.Flags) == 0 { return false } return hasProductFlag(item.Product.Flags, flagType) } } func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { return func(realization *entities.ExpenseRealization) bool { if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil { return false } return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType) } } func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { hasFlag := filterRealizationByNonstockFlag(flagType) return func(realization *entities.ExpenseRealization) bool { return !hasFlag(realization) } } func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 { amount := 0.0 for i := range items { if filter(&items[i]) { amount += extractor(&items[i]) } } return amount } func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter) } func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 { return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType)) } func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 { return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true }) } func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 { return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter) } func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 { return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter) } func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 { return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) } func isChickenProductFlag(flagType utils.FlagType) bool { switch flagType { case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati: return true } return false } func isEggProductFlag(flagType utils.FlagType) bool { switch flagType { case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah, utils.FlagTelurPutih, utils.FlagTelurRetak: return true } return false } func getSalesTypeFromProductFlags(product *entities.Product) string { if product == nil || len(product.Flags) == 0 { return PLSalesTypeChicken } for _, flag := range product.Flags { flagType := utils.FlagType(strings.ToUpper(flag.Name)) if isEggProductFlag(flagType) { return PLSalesTypeEgg } if isChickenProductFlag(flagType) { return PLSalesTypeChicken } } return PLSalesTypeChicken } func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { categorized := make(map[string][]entities.MarketingDeliveryProduct) for _, delivery := range deliveries { product := delivery.MarketingProduct.ProductWarehouse.Product salesType := getSalesTypeFromProductFlags(&product) categorized[salesType] = append(categorized[salesType], delivery) } return categorized } func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 { amount := 0.0 for _, delivery := range deliveries { amount += delivery.TotalPrice } return amount }