package service import ( "errors" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" 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" 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" "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) } // CostData holds all cost-related information type CostData struct { FeedCost float64 OvkCost float64 ChickenCost float64 ExpeditionCost float64 BudgetOperational float64 RealizationOperational float64 } // ProductionData holds all production and sales related information type ProductionData struct { TotalPopulationIn float64 TotalDepletion float64 TotalWeightProduced float64 TotalEggWeightKg float64 TotalWeightSold float64 TotalBirdSold float64 TotalSalesAmount float64 } type closingKeuanganService struct { Log *logrus.Logger ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository RecordingRepo recordingRepository.RecordingRepository HppSvc commonSvc.HppService HppRepo commonRepo.HppCostRepository } func NewClosingKeuanganService( projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, recordingRepo recordingRepository.RecordingRepository, hppSvc commonSvc.HppService, hppRepo commonRepo.HppCostRepository, ) ClosingKeuanganService { return &closingKeuanganService{ Log: utils.Log, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, HppSvc: hppSvc, HppRepo: hppRepo, } } 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") } projectFlockKandangs, 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, projectFlockKandangs) } 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 } projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") } if projectFlockKandang.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") } projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang} return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) } func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang) (*dto.ClosingKeuanganData, error) { var projectFlockKandangIDs []uint for _, projectFlockKandang := range projectFlockKandangs { projectFlockKandangIDs = append(projectFlockKandangIDs, projectFlockKandang.Id) } isPerKandang := len(projectFlockKandangs) == 1 var projectFlockKandangID *uint if isPerKandang { kandangID := projectFlockKandangs[0].Id projectFlockKandangID = &kandangID } costs, err := s.calculateCosts(c, projectFlock, projectFlockKandangs, projectFlockKandangIDs, projectFlockKandangID) if err != nil { return nil, err } productionData, err := s.calculateProductionData(c, projectFlock, projectFlockKandangIDs, projectFlockKandangID) if err != nil { return nil, err } hppSection := s.buildHPPSection(c, projectFlock, projectFlockKandangs, costs, productionData) profitLossSection := s.buildProfitLossSection(projectFlock, costs, productionData) data := dto.ToClosingKeuanganData(hppSection, profitLossSection) return &data, nil } func (s closingKeuanganService) calculateCosts(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*CostData, error) { costs := &CostData{} var err error costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { costs.FeedCost = 0 } costs.OvkCost, err = s.HppRepo.GetOvkUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { costs.OvkCost = 0 } if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { for _, projectFlockKandang := range projectFlockKandangs { depresiasiCost, err := s.HppSvc.GetDepresiasiTransfer(projectFlockKandang.Id, nil) if err == nil { costs.ChickenCost += depresiasiCost } pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) if err == nil { costs.ChickenCost += pulletCost } } } else { for _, projectFlockKandang := range projectFlockKandangs { pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) if err == nil { costs.ChickenCost += pulletCost } } } costs.ExpeditionCost, err = s.HppRepo.GetExpedisionCost(c.Context(), projectFlockKandangIDs) if err != nil { costs.ExpeditionCost = 0 } if budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlock.Id); err == nil { totalBudget := 0.0 for _, budget := range budgets { totalBudget += budget.Price * budget.Qty } if projectFlockKandangID != nil { allKandangs, errKandang := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) if errKandang == nil && len(allKandangs) > 0 { costs.BudgetOperational = totalBudget / float64(len(allKandangs)) } } else { costs.BudgetOperational = totalBudget } } else if !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Failed to fetch budgets for project_flock_id=%d: %+v", projectFlock.Id, err) } if realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID); err == nil { for _, realization := range realizations { amount := realization.Price * realization.Qty isEkspedisi := realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil && containsFlag(realization.ExpenseNonstock.Nonstock.Flags, "EKSPEDISI") if !isEkspedisi { costs.RealizationOperational += amount } } } else if !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Failed to fetch realizations for project_flock_id=%d: %+v", projectFlock.Id, err) } return costs, nil } func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*ProductionData, error) { data := &ProductionData{} var err error data.TotalPopulationIn, err = s.HppRepo.GetTotalPopulation(c.Context(), projectFlockKandangIDs) if err != nil { data.TotalPopulationIn = 0 } if projectFlockKandangID != nil { data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) } else { data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) } if err != nil { data.TotalDepletion = 0 } if projectFlockKandangID != nil { data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) } else { data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) } if err != nil { data.TotalWeightProduced = 0 } if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil) if err != nil { data.TotalEggWeightKg = 0 } } var deliveryProducts []entity.MarketingDeliveryProduct if projectFlockKandangID != nil { deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualanByCategory(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category) } else { deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualanByCategory(c.Context(), projectFlock.Id, nil, projectFlock.Category) } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan") } for _, delivery := range deliveryProducts { if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { continue } data.TotalWeightSold += delivery.TotalWeight data.TotalBirdSold += delivery.UsageQty data.TotalSalesAmount += delivery.TotalPrice } return data, nil } func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection { actualPopulation := production.TotalPopulationIn - production.TotalDepletion totalWeightProduced := production.TotalWeightProduced totalEggWeightKg := production.TotalEggWeightKg weightForCalculation := totalWeightProduced if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { weightForCalculation = totalEggWeightKg } calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } if weightForCalculation > 0 { rpPerKg = amount / weightForCalculation } return } createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem { budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount) realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount) return dto.ToHPPItem( id, category, code, label, dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), ) } hppItems := []dto.HPPItem{} hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost)) hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost)) docCode := string(dto.HPPCodeDOC) docLabel := "Pembelian DOC" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { docCode = string(dto.HPPCodeDepresiasi) docLabel = "Depresiasi" } hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost)) hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational)) hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost)) totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp) hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp) var eggBudgeting, eggRealization *dto.FinancialMetrics if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) { if *metrics == nil { *metrics = &dto.FinancialMetrics{ RpPerBird: 0, RpPerKg: rpPerKg, Amount: amount, } } else { (*metrics).Amount += amount if totalEggWeightKg > 0 { (*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg } } } for _, projectFlockKandang := range projectFlockKandangs { hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil) if err == nil { accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg) accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg) } } } hppSummary := dto.ToHPPSummary( "HPP", dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp), dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp), eggBudgeting, eggRealization, ) return dto.ToHPPSection(hppItems, hppSummary) } func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { totalWeightProduced := production.TotalWeightProduced totalEggWeightKg := production.TotalEggWeightKg totalSalesAmount := production.TotalSalesAmount totalWeightSold := production.TotalWeightSold totalBirdSold := production.TotalBirdSold actualPopulation := production.TotalPopulationIn - production.TotalDepletion isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying) // Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if isLaying { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } } else { if totalBirdSold > 0 { rpPerBird = amount / totalBirdSold } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } } return } // Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } if isLaying { if totalEggWeightKg > 0 { rpPerKg = amount / totalEggWeightKg } } else { if totalWeightProduced > 0 { rpPerKg = amount / totalWeightProduced } } return } // Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if isLaying { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } } else { if totalBirdSold > 0 { rpPerBird = amount / totalBirdSold } if totalWeightSold > 0 { rpPerKg = amount / totalWeightSold } } return } plItems := []dto.ProfitLossItem{} salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" if isLaying { salesLabel = "Penjualan Telur" } plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSales), salesLabel, "income", salesRpPerBird, salesRpPerKg, totalSalesAmount, )) totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost sapronakRpPerBird := 0.0 sapronakRpPerKg := 0.0 for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { rpPerBird, rpPerKg := calculateCostMetrics(amount) sapronakRpPerBird += rpPerBird sapronakRpPerKg += rpPerKg } plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), "Pengeluaran Sapronak", "purchase", sapronakRpPerBird, sapronakRpPerKg, totalSapronakAmount, )) overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", "overhead", overheadRpPerBird, overheadRpPerKg, costs.RealizationOperational, )) ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", "overhead", ekspedisiRpPerBird, ekspedisiRpPerKg, costs.ExpeditionCost, )) costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost costOfGoodsSoldRpPerBird := sapronakRpPerBird costOfGoodsSoldRpPerKg := sapronakRpPerKg grossProfit := totalSalesAmount - costOfGoodsSold grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg netProfit := grossProfit - totalOperatingExpenses netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg plSummary := dto.ToProfitLossSummary( dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit), dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses), dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit), ) return dto.ToProfitLossSection(plItems, plSummary) } func containsFlag(flags []entity.Flag, name string) bool { for _, flag := range flags { if flag.Name == name { return true } } return false }