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.ClosingKeuanganData, error) GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, 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.ClosingKeuanganData, error) { 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") } 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 } // 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") } return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) } func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } // Validate and fetch project flock kandang kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") } if kandang.ProjectFlockId != projectFlockID { return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock") } projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } 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 } kandangs := []entity.ProjectFlockKandang{*kandang} return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) } func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) { // Define flag filters using constants pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)} ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)} ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)} allFilters := append(pakanFilters, ovkFilters...) allFilters = append(allFilters, ayamFilters...) var allProductUsageRows []repository.ProductUsageRow // Get ALL product usage for _, kandang := range kandangs { rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters) if err == nil { allProductUsageRows = append(allProductUsageRows, rows...) } } // Classify into categories based on flag priority var pakanProductUsageRows []repository.ProductUsageRow var ovkProductUsageRows []repository.ProductUsageRow var ayamProductUsageRows []repository.ProductUsageRow 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 if hasPakanFlag { pakanProductUsageRows = append(pakanProductUsageRows, row) } else if hasOvkFlag { ovkProductUsageRows = append(ovkProductUsageRows, row) } else if hasAyamFlag { ayamProductUsageRows = append(ayamProductUsageRows, row) } else { continue } } // 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 } // Determine if this is per-kandang or per-project-flock scope isPerKandang := len(kandangs) == 1 var projectFlockKandangID *uint if isPerKandang { kandangID := kandangs[0].Id projectFlockKandangID = &kandangID } var err error // Fetch realizations var realizations []entity.ExpenseRealization if isPerKandang && projectFlockKandangID != nil { realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID) } else { realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil) } if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB { db = db.Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.Product") return db }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } // Filter by kandang if scope is per-kandang (manual filtering after fetch) if isPerKandang && projectFlockKandangID != nil { filteredProducts := make([]entity.MarketingDeliveryProduct, 0) for _, dp := range deliveryProducts { pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId if pfKandangID != nil && *pfKandangID == *projectFlockKandangID { filteredProducts = append(filteredProducts, dp) } } deliveryProducts = filteredProducts } // Fetch chickins var chickins []entity.ProjectChickin if isPerKandang && projectFlockKandangID != nil { chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID) } else { chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) } if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } // Get total depletion var totalDepletion float64 if isPerKandang && projectFlockKandangID != nil { totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) } else { totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) } if err != nil { totalDepletion = 0 } totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id) if err != nil { } // Try to get actual weight from uniformity data var totalWeightFromUniformity float64 if isPerKandang && projectFlockKandangID != nil { totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) } else { totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) } if err != nil { } else if totalWeightFromUniformity > 0 { totalWeightProduced = totalWeightFromUniformity } // Fetch egg data only for Laying category var totalEggWeightKg float64 if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { // TODO: Replace with actual method to get egg weight from RecordingRepo // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id) // For now, set to 0 as placeholder totalEggWeightKg = 0 } else { totalEggWeightKg = 0 } // Build new DTO structure // Calculate totals var totalPopulation float64 for _, chickin := range chickins { totalPopulation += chickin.UsageQty } // Calculate actual population (total population - depletion) actualPopulation := totalPopulation - totalDepletion // 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"}) 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 // 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) } } } // 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 } // 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 and total weight produced) calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if totalPopulation > 0 { rpPerBird = amount / totalPopulation } if totalWeightProduced > 0 { rpPerKg = amount / totalWeightProduced } return } // Build HPP Items using constants hppItems := []dto.HPPItem{} // PAKAN item pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan) pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice) hppItems = append(hppItems, dto.ToHPPItem( 1, "purchase", string(dto.HPPCodePakan), "Pembelian 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", string(dto.HPPCodeOVK), "Pembelian OVK", dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk), dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice), )) // DOC/DEPRESIASI item docCode := string(dto.HPPCodeDOC) docLabel := "Pembelian DOC" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { docCode = string(dto.HPPCodeDepresiasi) docLabel = "Depresiasi" } docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam) docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice) hppItems = append(hppItems, dto.ToHPPItem( 3, "purchase", docCode, docLabel, dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam), dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice), )) // OVERHEAD item overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational) overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization) hppItems = append(hppItems, dto.ToHPPItem( 4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational), dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization), )) // EKSPEDISI item ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi) ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization) hppItems = append(hppItems, dto.ToHPPItem( 5, "overhead", string(dto.HPPCodeEkspedisi), "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 using constants 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( string(dto.PLCodeSales), salesLabel, "income", salesRpPerBird, salesRpPerKg, totalSalesAmount, )) // SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), sapronakLabel, "purchase", sapronakRpPerBird, sapronakRpPerKg, totalSapronakAmount, )) // OVERHEAD item overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", "overhead", overheadRpPerBird, overheadRpPerKg, totalOperationalRealization, )) // EKSPEDISI item plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", "overhead", ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization, )) // Profit Loss Summary // Gross Profit = Sales - (DOC + PAKAN + OVK) only // Gross Profit should NOT include overhead and ekspedisi costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice costOfGoodsSoldRpPerBird := sapronakRpPerBird grossProfit := totalSalesAmount - costOfGoodsSold grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird // Operating Expenses (Overhead + Ekspedisi) totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird // Net Profit = Gross Profit - Operating Expenses netProfit := grossProfit - totalOperatingExpenses netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird 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) return &data, 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 }