package service import ( "errors" "strings" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) // ClosingKeuanganService handles closing keuangan business logic type ClosingKeuanganService interface { GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganResponse, error) } type closingKeuanganService struct { Log *logrus.Logger ClosingKeuanganRepo repository.ClosingKeuanganRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository RecordingRepo recordingRepository.RecordingRepository } func NewClosingKeuanganService( closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, recordingRepo recordingRepository.RecordingRepository, ) ClosingKeuanganService { return &closingKeuanganService{ Log: utils.Log, ClosingKeuanganRepo: closingKeuanganRepo, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, } } func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganResponse, error) { s.Log.Infof("===== START GetClosingKeuangan for ProjectFlockID: %d =====", projectFlockID) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } s.Log.Infof("ProjectFlock: ID=%d, Name=%s, Category=%s", projectFlock.Id, projectFlock.FlockName, projectFlock.Category) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } // Preload Nonstock.Flags manually var budgetIDs []uint for _, b := range budgets { budgetIDs = append(budgetIDs, b.Id) } if len(budgetIDs) > 0 { err = s.ProjectBudgetRepo.DB().WithContext(c.Context()). Preload("Nonstock.Flags"). Where("id IN ?", budgetIDs). Find(&budgets).Error if err != nil { s.Log.Warnf("Failed to preload Nonstock.Flags: %v", err) } } s.Log.Infof("Budgets fetched: %d items", len(budgets)) for i, b := range budgets { nonstockName := "Unknown" if b.Nonstock != nil { nonstockName = b.Nonstock.Name } s.Log.Infof(" Budget[%d]: ID=%d, Nonstock=%s, Price=%.2f, Qty=%.2f", i, b.Id, nonstockName, b.Price, b.Qty) } // Get all kandang for this project flock kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } s.Log.Infof("Kandangs fetched: %d kandangs", len(kandangs)) for i, k := range kandangs { s.Log.Infof(" Kandang[%d]: ID=%d, KandangID=%d, Period=%d", i, k.Id, k.KandangId, k.Period) } // Define flag filters // PAKAN flags: PAKAN, PRE-STARTER, STARTER, FINISHER (priority 1) // OVK flags: OVK, OBAT, VITAMIN, KIMIA, EKSPEDISI (priority 2) // AYAM flags: DOC, PULLET, LAYER (priority 3 - only if no PAKAN and OVK) pakanFilters := []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"} ovkFilters := []string{"OVK", "OBAT", "VITAMIN", "KIMIA", "EKSPEDISI"} ayamFilters := []string{"DOC", "PULLET", "LAYER"} allFilters := append(pakanFilters, ovkFilters...) allFilters = append(allFilters, ayamFilters...) s.Log.Infof("All Filters: %v", allFilters) var allProductUsageRows []repository.ProductUsageRow // Get ALL product usage once s.Log.Infof("===== Fetching ALL product usage =====") for _, kandang := range kandangs { s.Log.Infof("Fetching ALL for kandang ID=%d", kandang.Id) rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters) if err != nil { s.Log.Errorf("Failed to get product usage for kandang %d: %v", kandang.Id, err) } else { s.Log.Infof("Kandang %d: Got %d rows", kandang.Id, len(rows)) for i, row := range rows { s.Log.Infof(" [%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f", i, row.ProductID, row.ProductName, row.FlagNames, row.TotalQty, row.Price, row.TotalPengeluaran) } allProductUsageRows = append(allProductUsageRows, rows...) } } // Classify into categories based on flag priority var pakanProductUsageRows []repository.ProductUsageRow var ovkProductUsageRows []repository.ProductUsageRow var ayamProductUsageRows []repository.ProductUsageRow s.Log.Infof("===== Classifying products by flag priority =====") for _, row := range allProductUsageRows { // Parse flag names from comma-separated string flagNames := strings.Split(row.FlagNames, ",") hasPakanFlag := false hasOvkFlag := false hasAyamFlag := false for _, flag := range flagNames { flag = strings.TrimSpace(flag) if containsItem(pakanFilters, flag) { hasPakanFlag = true } if containsItem(ovkFilters, flag) { hasOvkFlag = true } if containsItem(ayamFilters, flag) { hasAyamFlag = true } } // Priority: PAKAN > OVK > AYAM category := "" if hasPakanFlag { pakanProductUsageRows = append(pakanProductUsageRows, row) category = "PAKAN" } else if hasOvkFlag { ovkProductUsageRows = append(ovkProductUsageRows, row) category = "OVK" } else if hasAyamFlag { ayamProductUsageRows = append(ayamProductUsageRows, row) category = "AYAM" } else { s.Log.Warnf("ProductID=%d (%s) has no recognized flags: %s", row.ProductID, row.ProductName, row.FlagNames) continue } s.Log.Infof("ProductID=%d (%s) → Category: %s (Flags: %s, Pakan=%v, OVK=%v, Ayam=%v)", row.ProductID, row.ProductName, category, row.FlagNames, hasPakanFlag, hasOvkFlag, hasAyamFlag) } s.Log.Infof("Total ProductUsageRows collected: %d items", len(allProductUsageRows)) s.Log.Infof(" - PAKAN: %d items", len(pakanProductUsageRows)) s.Log.Infof(" - OVK: %d items", len(ovkProductUsageRows)) s.Log.Infof(" - AYAM: %d items", len(ayamProductUsageRows)) // Calculate total price for each category var totalPakanPrice, totalOvkPrice, totalAyamPrice float64 for _, row := range pakanProductUsageRows { totalPakanPrice += row.TotalPengeluaran } for _, row := range ovkProductUsageRows { totalOvkPrice += row.TotalPengeluaran } for _, row := range ayamProductUsageRows { totalAyamPrice += row.TotalPengeluaran } s.Log.Infof("===== TOTAL PRICE BY CATEGORY =====") s.Log.Infof(" - PAKAN Total Price: %.2f", totalPakanPrice) s.Log.Infof(" - OVK Total Price: %.2f", totalOvkPrice) s.Log.Infof(" - AYAM Total Price: %.2f", totalAyamPrice) s.Log.Infof(" - ALL Total Price: %.2f", totalPakanPrice+totalOvkPrice+totalAyamPrice) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } s.Log.Infof("Realizations fetched: %d items", len(realizations)) for i, r := range realizations { s.Log.Infof(" Realization[%d]: ID=%d, Price=%.2f, Qty=%.2f", i, r.Id, r.Price, r.Qty) } deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { return db.Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.Product") }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } s.Log.Infof("DeliveryProducts fetched: %d items", len(deliveryProducts)) for i, dp := range deliveryProducts { s.Log.Infof(" DeliveryProduct[%d]: ID=%d, TotalWeight=%.2f, TotalPrice=%.2f", i, dp.Id, dp.TotalWeight, dp.TotalPrice) } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } s.Log.Infof("Chickins fetched: %d items", len(chickins)) for i, ch := range chickins { s.Log.Infof(" Chickin[%d]: ID=%d, UsageQty=%.2f", i, ch.Id, ch.UsageQty) } // Get total depletion totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) totalDepletion = 0 } s.Log.Infof("TotalDepletion: %.2f birds", totalDepletion) totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } s.Log.Infof("TotalWeightProduced (from stub): %.2f kg", totalWeightProduced) // Try to get actual weight from uniformity data totalWeightFromUniformity, err := s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalWeightProducedFromUniformityByProjectFlockID error: %v", err) } else if totalWeightFromUniformity > 0 { totalWeightProduced = totalWeightFromUniformity s.Log.Infof("TotalWeightProduced (from uniformity): %.2f kg", totalWeightProduced) } // Fetch egg data only for Laying category var totalEggWeightKg float64 if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { s.Log.Infof("===== Fetching EGG data (Laying Category) =====") // TODO: Replace with actual method to get egg weight from RecordingRepo // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlockID) // For now, set to 0 as placeholder totalEggWeightKg = 0 s.Log.Infof("TotalEggWeightKg: %.2f kg", totalEggWeightKg) if err != nil { s.Log.Warnf("Failed to fetch egg weight: %v", err) } } else { s.Log.Infof("Skipping egg data fetch (Category: %s - not Laying)", projectFlock.Category) totalEggWeightKg = 0 } // Build new DTO structure s.Log.Infof("===== BUILDING NEW DTO RESPONSE =====") // Calculate totals var totalPopulation float64 for _, chickin := range chickins { totalPopulation += chickin.UsageQty } // Calculate actual population (total population - depletion) actualPopulation := totalPopulation - totalDepletion s.Log.Infof("Population - Total: %.2f, Depletion: %.2f, Actual: %.2f", totalPopulation, totalDepletion, actualPopulation) // Calculate budget totals by category calculateBudgetByFlag := func(flags []string) float64 { var total float64 for _, budget := range budgets { if budget.Nonstock != nil { for _, nonstockFlag := range budget.Nonstock.Flags { flagName := strings.ToUpper(nonstockFlag.Name) for _, targetFlag := range flags { if flagName == strings.ToUpper(targetFlag) { total += budget.Price * budget.Qty break } } } } } return total } // Budget per category budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}) budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA", "EKSPEDISI"}) budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"}) budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"}) // Operational budget = total budget - pakan - ovk - ayam - ekspedisi totalBudgetAmount := 0.0 for _, budget := range budgets { totalBudgetAmount += budget.Price * budget.Qty } budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi s.Log.Infof("Budgets by category:") s.Log.Infof(" PAKAN: %.2f", budgetPakan) s.Log.Infof(" OVK: %.2f", budgetOvk) s.Log.Infof(" AYAM: %.2f", budgetAyam) s.Log.Infof(" OVERHEAD: %.2f", budgetOperational) s.Log.Infof(" EKSPEDISI: %.2f", budgetEkspedisi) // Calculate realization totals var totalRealizationAmount float64 var totalEkspedisiRealization float64 for _, realization := range realizations { amount := realization.Price * realization.Qty totalRealizationAmount += amount // Check if this is ekspedisi (need to check nonstock flags) if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil { for _, flag := range realization.ExpenseNonstock.Nonstock.Flags { if flag.Name == "EKSPEDISI" { totalEkspedisiRealization += amount break } } } } totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization // Filter delivery products based on category var filteredDeliveryProducts []entity.MarketingDeliveryProduct for _, delivery := range deliveryProducts { // Get product from delivery if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { continue } product := delivery.MarketingProduct.ProductWarehouse.Product isEggProduct := false isChickenProduct := false // Check product flags for _, flag := range product.Flags { flagName := strings.ToUpper(flag.Name) // Egg product flags if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" || flagName == "TELURPUTIH" || flagName == "TELURRETAK" { isEggProduct = true } // Chicken product flags if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" { isChickenProduct = true } } // Filter based on project flock category if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { // Laying: only egg products if isEggProduct { filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) } } else { // Growing/Contract Growing: only chicken products if isChickenProduct || (!isEggProduct && !isChickenProduct) { // Include if chicken product or if no specific flags (default to chicken) filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) } } } s.Log.Infof("Filtered DeliveryProducts: %d items (from %d total)", len(filteredDeliveryProducts), len(deliveryProducts)) // Calculate total weight sold and sales amount from filtered products var totalWeightSold float64 var totalSalesAmount float64 for _, delivery := range filteredDeliveryProducts { totalWeightSold += delivery.TotalWeight totalSalesAmount += delivery.TotalPrice } s.Log.Infof("Calculated totals:") s.Log.Infof(" Total Population: %.2f", totalPopulation) s.Log.Infof(" Actual Population: %.2f (after depletion)", actualPopulation) s.Log.Infof(" Total Weight Produced: %.2f kg (ayam)", totalWeightProduced) s.Log.Infof(" Total Weight Sold: %.2f kg (filtered by category)", totalWeightSold) s.Log.Infof(" Total Budget: %.2f", totalBudgetAmount) s.Log.Infof(" Total Realization: %.2f (Operational: %.2f, Ekspedisi: %.2f)", totalRealizationAmount, totalOperationalRealization, totalEkspedisiRealization) s.Log.Infof(" Total Sales: %.2f", totalSalesAmount) // Calculate metrics - always use kg ayam for rp_per_kg calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { rpPerBird = amount / actualPopulation // Use actual population } if totalWeightProduced > 0 { rpPerKg = amount / totalWeightProduced } return } // Calculate metrics for profit loss (use total population for sales) calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if totalPopulation > 0 { rpPerBird = amount / totalPopulation } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } return } // Build HPP Items hppItems := []dto.HPPItem{} // PAKAN item pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan) pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice) hppItems = append(hppItems, dto.ToHPPItem( 1, "purchase", "PAKAN", "Pakan", dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan), dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice), )) // OVK item ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk) ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice) hppItems = append(hppItems, dto.ToHPPItem( 2, "purchase", "OVK", "OVK", dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk), dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice), )) // DOC item docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam) docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice) hppItems = append(hppItems, dto.ToHPPItem( 3, "purchase", "DOC", "DOC", dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam), dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice), )) // OVERHEAD operational item (before EKSPEDISI) overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational) overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization) hppItems = append(hppItems, dto.ToHPPItem( 4, "overhead", "OVERHEAD", "Pengeluaran Overhead", dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational), dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization), )) // EKSPEDISI item (overhead) ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi) ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization) hppItems = append(hppItems, dto.ToHPPItem( 5, "overhead", "EKSPEDISI", "Beban Ekspedisi", dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi), dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization), )) // HPP Summary totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp) hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp) var eggBudgeting, eggRealization *dto.FinancialMetrics if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 { eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg eggBudgeting = &dto.FinancialMetrics{ RpPerBird: 0, RpPerKg: eggBudgetRpPerKg, Amount: totalBudgetHpp, } eggRealization = &dto.FinancialMetrics{ RpPerBird: 0, RpPerKg: eggRealizationRpPerKg, Amount: totalRealizationHpp, } } hppSummary := dto.ToHPPSummary( "HPP", dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp), dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp), eggBudgeting, eggRealization, ) hppSection := dto.ToHPPSection(hppItems, hppSummary) // Build Profit Loss Items plItems := []dto.ProfitLossItem{} // SALES item salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { salesLabel = "Penjualan Telur" } plItems = append(plItems, dto.ToProfitLossItem( "SALES", salesLabel, "income", salesRpPerBird, salesRpPerKg, totalSalesAmount, )) // PURCHASE_DOC item purchaseDocLabel := "Pembelian DOC" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { purchaseDocLabel = "Depresiasi" } plItems = append(plItems, dto.ToProfitLossItem( "PURCHASE_DOC", purchaseDocLabel, "purchase", docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice, )) // PAKAN item plItems = append(plItems, dto.ToProfitLossItem( "PAKAN", "Pakan", "purchase", pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice, )) // OVK item plItems = append(plItems, dto.ToProfitLossItem( "OVK", "OVK", "purchase", ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice, )) // OVERHEAD item overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization) plItems = append(plItems, dto.ToProfitLossItem( "OVERHEAD", "Overhead", "overhead", overheadRpPerBird, overheadRpPerKg, totalOperationalRealization, )) // EKSPEDISI item plItems = append(plItems, dto.ToProfitLossItem( "EKSPEDISI", "Ekspedisi", "overhead", ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization, )) // Profit Loss Summary // Calculate total cost of goods sold (HPP) - use realization totalCostOfGoodsSold := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization totalCostOfGoodsSoldRpPerBird := pakanRealizationRpPerBird + ovkRealizationRpPerBird + docRealizationRpPerBird + overheadRpPerBird + ekspedisiRealizationRpPerBird // Gross Profit = Sales - Cost of Goods Sold grossProfit := totalSalesAmount - totalCostOfGoodsSold grossProfitRpPerBird := salesRpPerBird - totalCostOfGoodsSoldRpPerBird // Operating Expenses (already included in COGS above, so this shows the breakdown) totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird // Net Profit = Gross Profit (COGS already deducted) netProfit := grossProfit netProfitRpPerBird := grossProfitRpPerBird plSummary := dto.ToProfitLossSummary( dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit), dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses), dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit), ) profitLossSection := dto.ToProfitLossSection(plItems, plSummary) // Build complete response data := dto.ToClosingKeuanganData(hppSection, profitLossSection) response := dto.ToSuccessClosingKeuanganResponse(data) s.Log.Infof("===== NEW DTO RESPONSE BUILT SUCCESSFULLY =====") s.Log.Infof("HPP Items: %d, Profit Loss Items: %d", len(hppItems), len(plItems)) s.Log.Infof("===== END GetClosingKeuangan for ProjectFlockID: %d =====", projectFlockID) return &response, nil } // containsItem checks if a string exists in a slice func containsItem(slice []string, item string) bool { for _, s := range slice { if strings.EqualFold(s, item) { return true } } return false }