package dto import ( "strings" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === 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"` Comparison } 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"` 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 "Pembelian " + string(flagType) } func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []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) // Check if valid flag and not processed isValid := false for _, validFlag := range flags { if validFlag == flagType { isValid = true break } } if isValid && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) items = append(items, HppItem{ Type: getFlagLabel(flagType), Comparison: ToComparison( ToFinancialMetrics(rpPerBird, rpPerKg, amount), ToFinancialMetrics(rpPerBird, rpPerKg, amount), // Same for purchase ), }) seenFlags[flagType] = true } } } return items } // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup { items := []HppItem{} // Overhead: all budgets vs (all expenses EXCEPT ekspedisi) budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightSold) realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) if budgetAmount > 0 || realizationAmount > 0 { items = append(items, HppItem{ Type: "Pengeluaran Overhead", Comparison: ToComparison( ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), ), }) } // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold) if ekspedisiAmount > 0 { items = append(items, HppItem{ Type: "Beban Ekspedisi", Comparison: ToComparison( ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization ), }) } return HppGroup{ GroupName: "HPP dan Bahan Baku", Data: items, } } // === HPP SUMMARY === func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp { // Budget: purchases + budgets purchaseTotal := sumPurchaseTotal(purchaseItems) budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) totalBudget := purchaseTotal + budgetTotal // Realization: all expenses totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold) realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold) return SummaryHpp{ Label: label, Comparison: ToComparison( ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), ), } } func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection { hppGroups := []HppGroup{ { GroupName: "HPP dan Pengeluaran", Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation), }, ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation), } summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) 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 sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { for _, item := range items { totalAmount += item.Amount totalPerBird += item.RpPerBird } return } func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { // Categorize deliveries by sales type based on Product flags categorized := categorizeDeliveriesBySalesType(deliveryProducts) items := []PLItem{} // Process each sales category for salesType, deliveries := range categorized { amount := sumDeliveriesByCategory(deliveries) // Use totalPopulation and totalWeightSold for per-unit calculations rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) items = append(items, ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))) } return items } func ToPembelianItems(purchases []entities.PurchaseItem, totalPopulation, totalWeightSold float64) []PLItem { amount := sumPurchasesByFilter(purchases, func(item *entities.PurchaseItem) bool { if item.Product == nil || len(item.Product.Flags) == 0 { return false } for _, flag := range item.Product.Flags { flagType := strings.ToUpper(flag.Name) if flagType == string(utils.FlagDOC) || flagType == string(utils.FlagOVK) || flagType == string(utils.FlagPakan) { return true } } return false }) rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) return []PLItem{ ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } } func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) return []PLItem{ ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), } } func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) return []PLItem{ ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } } func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) totalOverhead, _ := sumPLItems(overheadItems) totalEkspedisi, _ := sumPLItems(ekspedisiItems) grossProfit := totalPenjualan - totalPembelian grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird totalOtherExpenses := totalOverhead + totalEkspedisi netProfit := grossProfit - totalOtherExpenses netProfitPerBird := grossProfitPerBird - 0.0 return PLSummaryGroup{ GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(0, 0, totalOtherExpenses)), NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), } } func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ProfitLossData{ Penjualan: penjualanItems, Pembelian: pembelianItems, Summary: summary, } } func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection { return ProfitLossSection{ Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems), } } func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { return ReportResponse{ HppPurchases: hppPurchases, ProfitLoss: profitLoss, } } // === MAIN BUILDER === func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse { var totalPopulation float64 var totalWeightSold float64 for _, chickin := range chickins { totalPopulation += chickin.UsageQty } for _, delivery := range deliveryProducts { totalWeightSold += delivery.TotalWeight } hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold) overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold) ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold) 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 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 } for _, flag := range item.Product.Flags { if strings.ToUpper(flag.Name) == string(flagType) { return true } } return false } } 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 } nonstock := realization.ExpenseNonstock.Nonstock for _, flag := range nonstock.Flags { if strings.ToUpper(flag.Name) == string(flagType) { return true } } return false } } func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { hasFlag := filterRealizationByNonstockFlag(flagType) return func(realization *entities.ExpenseRealization) bool { return !hasFlag(realization) } } func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { amount := 0.0 for i := range purchases { if filter(&purchases[i]) { amount += purchases[i].TotalPrice } } return amount } func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 { return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType)) } func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 { amount := 0.0 for i := range purchases { amount += purchases[i].TotalPrice } return amount } func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 { amount := 0.0 for i := range budgets { if filter(&budgets[i]) { amount += budgets[i].Price * budgets[i].Qty } } return amount } func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 { amount := 0.0 for i := range realizations { if filter(&realizations[i]) { amount += realizations[i].Price * realizations[i].Qty } } return amount } 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 "Penjualan Ayam Besar" } for _, flag := range product.Flags { flagType := utils.FlagType(strings.ToUpper(flag.Name)) if isEggProductFlag(flagType) { return "Penjualan Telur" } if isChickenProductFlag(flagType) { return "Penjualan Ayam Besar" } } return "Penjualan Ayam Besar" } 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 }