diff --git a/internal/modules/closings/dto/closingKeuanganResponse.dto.go b/internal/modules/closings/dto/closingKeuanganResponse.dto.go new file mode 100644 index 00000000..7aad48cc --- /dev/null +++ b/internal/modules/closings/dto/closingKeuanganResponse.dto.go @@ -0,0 +1,186 @@ +package dto + +// New Closing Keuangan Response DTO Structure + +// Base metrics - digunakan di banyak tempat +type NewFinancialMetrics struct { + RpPerBird float64 `json:"rp_per_bird"` + RpPerKg float64 `json:"rp_per_kg"` + Amount float64 `json:"amount"` +} + +// Comparison untuk Budgeting vs Realization +type NewComparison struct { + Budgeting NewFinancialMetrics `json:"budgeting"` + Realization NewFinancialMetrics `json:"realization"` +} + +// HPP Purchase Section +type HppPurchase struct { + Pakan NewComparison `json:"pakan"` + OVK NewComparison `json:"OVK"` + DOC NewComparison `json:"DOC"` + Depresiasi NewComparison `json:"Depresiasi"` +} + +// HPP Overhead Section +type HppOverhead struct { + Overhead NewComparison `json:"overhead"` + Ekspedisi NewComparison `json:"ekspedisi"` +} + +// Summary HPP +type NewSummaryHpp struct { + Label string `json:"label"` + Budgeting NewFinancialMetrics `json:"budgeting"` + Realization NewFinancialMetrics `json:"realization"` + EggBudgeting *NewFinancialMetrics `json:"egg_budgeting,omitempty"` + EggRealization *NewFinancialMetrics `json:"egg_realization,omitempty"` +} + +// HPP wrapper +type NewHpp struct { + HppPurchase HppPurchase `json:"hpp_purchase"` + HppOverhead HppOverhead `json:"hpp_overhead"` + SummaryHpp NewSummaryHpp `json:"summary_hpp"` +} + +// Purchase Cost (dengan type field, embedded NewFinancialMetrics) +type PurchaseCost struct { + Type string `json:"type"` + NewFinancialMetrics +} + +// PL Summary +type PLSummary struct { + GrossProfit NewFinancialMetrics `json:"gross_profit"` + SubTotal NewFinancialMetrics `json:"sub_total"` + NetProfit NewFinancialMetrics `json:"net_profit"` +} + +// Profit Loss wrapper +type NewProfitLoss struct { + PenjualanTelur NewFinancialMetrics `json:"penjualan_telur"` + PurchaseCost PurchaseCost `json:"purchase_cost"` + Overhead NewFinancialMetrics `json:"overhead"` + Ekspedisi NewFinancialMetrics `json:"ekspedisi"` + Summary PLSummary `json:"summary"` +} + +// Main Data structure +type NewClosingKeuanganData struct { + Hpp NewHpp `json:"hpp"` + ProfitLoss NewProfitLoss `json:"profit_loss"` +} + +// Full Response DTO +type NewClosingKeuanganResponse struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Data NewClosingKeuanganData `json:"data"` +} + +// === MAPPER FUNCTIONS === + +// ToNewFinancialMetrics creates a new financial metrics +func ToNewFinancialMetrics(rpPerBird, rpPerKg, amount float64) NewFinancialMetrics { + return NewFinancialMetrics{ + RpPerBird: rpPerBird, + RpPerKg: rpPerKg, + Amount: amount, + } +} + +// ToNewComparison creates a new budgeting vs realization comparison +func ToNewComparison(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount, realizationRpPerBird, realizationRpPerKg, realizationAmount float64) NewComparison { + return NewComparison{ + Budgeting: ToNewFinancialMetrics(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount), + Realization: ToNewFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + } +} + +// ToHppPurchase creates HPP purchase section +func ToHppPurchase(pakan, oVK, dOC, depresiasi NewComparison) HppPurchase { + return HppPurchase{ + Pakan: pakan, + OVK: oVK, + DOC: dOC, + Depresiasi: depresiasi, + } +} + +// ToHppOverhead creates HPP overhead section +func ToHppOverhead(overhead, ekspedisi NewComparison) HppOverhead { + return HppOverhead{ + Overhead: overhead, + Ekspedisi: ekspedisi, + } +} + +// ToNewSummaryHpp creates HPP summary +func ToNewSummaryHpp(label string, budgeting, realization NewFinancialMetrics, eggBudgeting, eggRealization *NewFinancialMetrics) NewSummaryHpp { + return NewSummaryHpp{ + Label: label, + Budgeting: budgeting, + Realization: realization, + EggBudgeting: eggBudgeting, + EggRealization: eggRealization, + } +} + +// ToNewHpp creates complete HPP section +func ToNewHpp(hppPurchase HppPurchase, hppOverhead HppOverhead, summaryHpp NewSummaryHpp) NewHpp { + return NewHpp{ + HppPurchase: hppPurchase, + HppOverhead: hppOverhead, + SummaryHpp: summaryHpp, + } +} + +// ToPurchaseCost creates purchase cost item +func ToPurchaseCost(costType string, metrics NewFinancialMetrics) PurchaseCost { + return PurchaseCost{ + Type: costType, + NewFinancialMetrics: metrics, + } +} + +// ToPLSummary creates profit loss summary +func ToPLSummary(grossProfit, subTotal, netProfit NewFinancialMetrics) PLSummary { + return PLSummary{ + GrossProfit: grossProfit, + SubTotal: subTotal, + NetProfit: netProfit, + } +} + +// ToNewProfitLoss creates complete profit loss section +func ToNewProfitLoss(penjualanTelur, overhead, ekspedisi NewFinancialMetrics, purchaseCost PurchaseCost, summary PLSummary) NewProfitLoss { + return NewProfitLoss{ + PenjualanTelur: penjualanTelur, + PurchaseCost: purchaseCost, + Overhead: overhead, + Ekspedisi: ekspedisi, + Summary: summary, + } +} + +// ToNewClosingKeuanganData creates complete closing keuangan data +func ToNewClosingKeuanganData(hpp NewHpp, profitLoss NewProfitLoss) NewClosingKeuanganData { + return NewClosingKeuanganData{ + Hpp: hpp, + ProfitLoss: profitLoss, + } +} + +// ToSuccessNewClosingKeuanganResponse creates success response shortcut +func ToSuccessNewClosingKeuanganResponse(data NewClosingKeuanganData) NewClosingKeuanganResponse { + return NewClosingKeuanganResponse{ + Code: 200, + Status: "success", + Message: "Get closing keuangan successfully", + Data: data, + } +} + diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index a79c9f0b..0830ea08 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -25,6 +25,7 @@ type ClosingModule struct{} func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { closingRepo := rClosing.NewClosingRepository(db) + closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db) userRepo := rUser.NewUserRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) @@ -40,7 +41,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 2ce3e496..f2d668a7 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -10,7 +10,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) @@ -24,7 +23,7 @@ type ClosingRepository interface { SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) - GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) + GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) @@ -33,8 +32,6 @@ type ClosingRepository interface { FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) - GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) - GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) } type ClosingRepositoryImpl struct { @@ -64,11 +61,6 @@ type SapronakRow struct { Notes string `gorm:"column:notes"` } -type ExpeditionHPPRow struct { - SupplierName string `gorm:"column:supplier_name"` - TotalAmount float64 `gorm:"column:total_amount"` -} - type SapronakQueryParams struct { Type string WarehouseIDs []uint @@ -127,219 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak return rows, totalResults, nil } -func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { - if len(projectFlockKandangIDs) == 0 { - return 0, 0, nil - } - - var purchaseAgg struct { - TotalIn float64 `gorm:"column:total_in"` - } - - err := r.DB().WithContext(ctx). - Table("purchase_items pi"). - Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). - Where("f.name = ?", "PAKAN"). - Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). - Scan(&purchaseAgg).Error - if err != nil { - return 0, 0, err - } - - var usageAgg struct { - TotalUsed float64 `gorm:"column:total_used"` - } - - err = r.DB().WithContext(ctx). - Table("recording_stocks rs"). - Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). - Joins("JOIN products prod ON prod.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). - Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Where("f.name = ?", "PAKAN"). - Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). - Scan(&usageAgg).Error - if err != nil { - return 0, 0, err - } - - return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil -} - -func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { - if len(projectFlockKandangIDs) == 0 { - return 0, nil - } - - var total float64 - if err := r.DB().WithContext(ctx). - Model(&entity.ProjectChickin{}). - Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). - Select("COALESCE(SUM(usage_qty), 0)"). - Scan(&total).Error; err != nil { - return 0, err - } - - return total, nil -} - -func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { - if len(projectFlockKandangIDs) == 0 { - return 0, nil - } - - var agg struct { - Total float64 `gorm:"column:total_culling"` - } - - err := r.DB().WithContext(ctx). - Table("recording_depletions rd"). - Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). - Joins("JOIN products prod ON prod.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). - Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Where("f.name = ?", utils.FlagAyamCulling). - Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). - Scan(&agg).Error - if err != nil { - return 0, err - } - - return agg.Total, nil -} - -func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { - if len(projectFlockKandangIDs) == 0 { - return 0, 0, 0, nil - } - - var agg struct { - TotalWeight float64 `gorm:"column:total_weight"` - TotalQty float64 `gorm:"column:total_qty"` - TotalPrice float64 `gorm:"column:total_price"` - } - - err := r.DB().WithContext(ctx). - Table("marketing_products mp"). - Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). - Scan(&agg).Error - if err != nil { - return 0, 0, 0, err - } - - return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil -} - -func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { - if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { - return 0, 0, 0, nil - } - - var agg struct { - TotalWeight float64 `gorm:"column:total_weight"` - TotalQty float64 `gorm:"column:total_qty"` - TotalPrice float64 `gorm:"column:total_price"` - } - - err := r.DB().WithContext(ctx). - Table("marketing_products mp"). - Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Joins("JOIN products prod ON prod.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). - Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Where("f.name IN ?", flagNames). - Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). - Scan(&agg).Error - if err != nil { - return 0, 0, 0, err - } - - return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil -} - -func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { - if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { - return 0, nil - } - - var agg struct { - TotalQty float64 `gorm:"column:total_qty"` - } - - err := r.DB().WithContext(ctx). - Table("recording_eggs re"). - Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). - Joins("JOIN products prod ON prod.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). - Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Where("f.name IN ?", flagNames). - Select("COALESCE(SUM(re.qty), 0) AS total_qty"). - Scan(&agg).Error - if err != nil { - return 0, err - } - - return agg.TotalQty, nil -} - -func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) { - if fcrID == 0 { - return []entity.FcrStandard{}, nil - } - - var standards []entity.FcrStandard - if err := r.DB().WithContext(ctx). - Where("fcr_id = ?", fcrID). - Order("weight ASC"). - Find(&standards).Error; err != nil { - return nil, err - } - - return standards, nil -} - -func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { - db := r.DB().WithContext(ctx) - - if projectFlockID == 0 { - return nil, fmt.Errorf("invalid project flock id") - } - - query := db. - Table("expense_realizations AS er"). - Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id"). - Joins("JOIN expenses e ON e.id = ens.expense_id"). - Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id"). - Joins("JOIN nonstocks n ON n.id = ens.nonstock_id"). - Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock). - Joins("JOIN suppliers s ON s.id = e.supplier_id"). - Where("pfk.project_flock_id = ?", projectFlockID). - Where("e.category = ?", "BOP"). - Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) - - if projectFlockKandangID != nil && *projectFlockKandangID != 0 { - query = query.Where("pfk.id = ?", *projectFlockKandangID) - } - - var rows []ExpeditionHPPRow - err := query. - Select( - "e.supplier_id AS supplier_id, " + - "s.name AS supplier_name, " + - "SUM(er.qty * er.price) AS total_amount", - ). - Group("e.supplier_id, s.name"). - Scan(&rows).Error - if err != nil { - return nil, err - } - - return rows, nil -} - const ( sapronakIncomingPurchasesSQL = ` SELECT @@ -902,130 +681,180 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand return in, out, nil } -type ActualUsageCostRow struct { - ProductID uint `gorm:"column:product_id"` - ProductName string `gorm:"column:product_name"` - FlagName string `gorm:"column:flag_name"` - TotalQty float64 `gorm:"column:total_qty"` - TotalPrice float64 `gorm:"column:total_price"` - AveragePrice float64 `gorm:"column:average_price"` +// === CLOSING DATA PRODUKSI METHODS === + +func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, nil + } + + var purchaseAgg struct { + TotalIn float64 `gorm:"column:total_in"` + } + + err := r.DB().WithContext(ctx). + Table("purchase_items pi"). + Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). + Where("f.name = ?", "PAKAN"). + Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). + Scan(&purchaseAgg).Error + if err != nil { + return 0, 0, err + } + + var usageAgg struct { + TotalUsed float64 `gorm:"column:total_used"` + } + + err = r.DB().WithContext(ctx). + Table("recording_stocks rs"). + Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", "PAKAN"). + Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). + Scan(&usageAgg).Error + if err != nil { + return 0, 0, err + } + + return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil } -func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) { - if projectFlockID == 0 { - return []ActualUsageCostRow{}, nil +func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil } - db := r.DB().WithContext(ctx) + var total float64 + if err := r.DB().WithContext(ctx). + Model(&entity.ProjectChickin{}). + Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(usage_qty), 0)"). + Scan(&total).Error; err != nil { + return 0, err + } - // Get all project flock kandang IDs for this project flock - var pfkIDs []uint - err := db.Table("project_flock_kandangs"). - Where("project_flock_id = ?", projectFlockID). - Pluck("id", &pfkIDs).Error + return total, nil +} + +func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var agg struct { + Total float64 `gorm:"column:total_culling"` + } + + err := r.DB().WithContext(ctx). + Table("recording_depletions rd"). + Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", utils.FlagAyamCulling). + Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). + Scan(&agg).Error if err != nil { + return 0, err + } + + return agg.Total, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, nil + } + + var agg struct { + TotalQty float64 `gorm:"column:total_qty"` + } + + err := r.DB().WithContext(ctx). + Table("recording_eggs re"). + Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(re.qty), 0) AS total_qty"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.TotalQty, nil +} + +func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) { + if fcrID == 0 { + return []entity.FcrStandard{}, nil + } + + var standards []entity.FcrStandard + if err := r.DB().WithContext(ctx). + Where("fcr_id = ?", fcrID). + Order("weight ASC"). + Find(&standards).Error; err != nil { return nil, err } - if len(pfkIDs) == 0 { - return []ActualUsageCostRow{}, nil - } - - var rows []ActualUsageCostRow - - purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() - transferStockableKey := fifo.StockableKeyStockTransferIn.String() - - recordingQuery := db. - Table("recordings AS r"). - Select(` - pw.product_id AS product_id, - p.name AS product_name, - COALESCE(f.name, tf.name) AS flag_name, - COALESCE(SUM( - CASE - WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) - WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) - ELSE 0 - END - ), 0) AS total_qty, - COALESCE(SUM( - CASE - WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) - ELSE 0 - END - ), 0) AS total_price, - COALESCE(SUM( - CASE - WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) - WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) - ELSE 0 - END - ), 0) AS qty_divisor, - COALESCE(SUM( - CASE - WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) - ELSE 0 - END - ), 0) / NULLIF(COALESCE(SUM( - CASE - WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) - WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) - ELSE 0 - END - ), 0), 0) AS average_price`, - purchaseStockableKey, transferStockableKey, - purchaseStockableKey, transferStockableKey, - purchaseStockableKey, transferStockableKey, - purchaseStockableKey, transferStockableKey, - purchaseStockableKey, transferStockableKey). - Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). - Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). - Joins("JOIN products AS p ON p.id = pw.product_id"). - Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", - "recording_stocks", entity.StockAllocationStatusActive). - Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey). - Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey). - Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id"). - Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id"). - Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). - Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct). - Where("r.project_flock_kandangs_id IN ?", pfkIDs). - Where("r.deleted_at IS NULL"). - Group("pw.product_id, p.name, COALESCE(f.name, tf.name)") - - if err := recordingQuery.Scan(&rows).Error; err != nil { - return nil, err - } - - chickinQuery := db. - Table("project_chickins AS pc"). - Select(` - pw.product_id AS product_id, - p.name AS product_name, - f.name AS flag_name, - COALESCE(SUM(pc.usage_qty), 0) AS total_qty, - COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price, - COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price - `). - Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id"). - Joins("JOIN products AS p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id"). - Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). - Where("pc.project_flock_kandang_id IN ?", pfkIDs). - Where("pc.usage_qty > 0"). - Group("pw.product_id, p.name, f.name") - - var chickinRows []ActualUsageCostRow - if err := chickinQuery.Scan(&chickinRows).Error; err != nil { - return nil, err - } - - rows = append(rows, chickinRows...) - - return rows, nil + return standards, nil } func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { diff --git a/internal/modules/closings/repositories/closingKeuangan.repository.go b/internal/modules/closings/repositories/closingKeuangan.repository.go new file mode 100644 index 00000000..5dedfa0e --- /dev/null +++ b/internal/modules/closings/repositories/closingKeuangan.repository.go @@ -0,0 +1,611 @@ +package repository + +import ( + "context" + "fmt" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" + "gorm.io/gorm" +) + +// ClosingKeuanganRepository handles database operations for closing keuangan +type ClosingKeuanganRepository interface { + repository.BaseRepository[interface{}] + + // Egg Production + GetTotalEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalQty int, totalWeightKg float64, err error) + GetTotalEggProductionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (totalQty int, totalWeightKg float64, totalRecordings int, err error) + GetEggProductionByProjectFlockKandangIDsWithDetails(ctx context.Context, projectFlockKandangIDs []uint) ([]EggProductionDetailRow, error) + GetCumulativeEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalQty int, totalWeightKg float64, err error) + + // Population Data + GetTotalPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (totalPopulation float64, err error) + GetTotalPopulationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalPopulation float64, err error) + GetRemainingPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (remainingPopulation float64, err error) + + // Budget Data + GetTotalBudgetByProjectFlockID(ctx context.Context, projectFlockID uint) (totalBudget float64, err error) + GetTotalBudgetByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalBudget float64, err error) + + // Realization/Expense Data + GetTotalRealizationByProjectFlockID(ctx context.Context, projectFlockID uint) (totalRealization float64, err error) + GetTotalRealizationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (totalRealization float64, err error) + GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) + + // Expedition + GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) + + // Products + GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) +} + +type ClosingKeuanganRepositoryImpl struct { + *repository.BaseRepositoryImpl[interface{}] +} + +func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository { + return &ClosingKeuanganRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db), + } +} + +// Result Rows + +type EggProductionDetailRow struct { + ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"` + KandangName string `gorm:"column:kandang_name"` + TotalQty int `gorm:"column:total_qty"` + TotalWeightKg float64 `gorm:"column:total_weight_kg"` + TotalRecordings int `gorm:"column:total_recordings"` +} + +type ExpeditionHPPRow struct { + SupplierName string `gorm:"column:supplier_name"` + TotalAmount float64 `gorm:"column:total_amount"` +} + +type ActualUsageCostRow struct { + ProductID uint `gorm:"column:product_id"` + ProductName string `gorm:"column:product_name"` + FlagName string `gorm:"column:flag_name"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + AveragePrice float64 `gorm:"column:average_price"` +} + +// === EGG PRODUCTION QUERIES === + +// GetTotalEggProductionByProjectFlockID gets total egg production for all kandangs in a project flock +func (r *ClosingKeuanganRepositoryImpl) GetTotalEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (int, float64, error) { + if projectFlockID == 0 { + return 0, 0, nil + } + + var result struct { + TotalQty float64 + TotalWeightKg float64 + } + + err := r.DB().WithContext(ctx). + Table("project_flocks pf"). + Select(` + COALESCE(SUM(re.qty), 0) AS total_qty, + COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg + `). + Joins("JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id"). + Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id"). + Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id"). + Where("pf.id = ?", projectFlockID). + Where("pf.deleted_at IS NULL"). + Where("r.deleted_at IS NULL"). + Scan(&result).Error + + if err != nil { + return 0, 0, err + } + + return int(result.TotalQty), result.TotalWeightKg, nil +} + +// GetTotalEggProductionByProjectFlockKandangID gets total egg production for a specific kandang +func (r *ClosingKeuanganRepositoryImpl) GetTotalEggProductionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int, float64, int, error) { + if projectFlockKandangID == 0 { + return 0, 0, 0, nil + } + + var result struct { + TotalQty float64 + TotalWeightKg float64 + TotalRecordings int + } + + err := r.DB().WithContext(ctx). + Table("project_flock_kandangs pfk"). + Select(` + COALESCE(SUM(re.qty), 0) AS total_qty, + COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg, + COUNT(DISTINCT r.id) AS total_recordings + `). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id"). + Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id"). + Where("pfk.id = ?", projectFlockKandangID). + Where("pf.deleted_at IS NULL"). + Where("r.deleted_at IS NULL"). + Scan(&result).Error + + if err != nil { + return 0, 0, 0, err + } + + return int(result.TotalQty), result.TotalWeightKg, result.TotalRecordings, nil +} + +// GetEggProductionByProjectFlockKandangIDsWithDetails gets egg production details for multiple kandangs +func (r *ClosingKeuanganRepositoryImpl) GetEggProductionByProjectFlockKandangIDsWithDetails(ctx context.Context, projectFlockKandangIDs []uint) ([]EggProductionDetailRow, error) { + if len(projectFlockKandangIDs) == 0 { + return []EggProductionDetailRow{}, nil + } + + var results []EggProductionDetailRow + + err := r.DB().WithContext(ctx). + Table("project_flock_kandangs pfk"). + Select(` + pfk.id AS project_flock_kandang_id, + k.name AS kandang_name, + COALESCE(SUM(re.qty), 0) AS total_qty, + COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg, + COUNT(DISTINCT r.id) AS total_recordings + `). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("JOIN kandangs k ON k.id = pfk.kandang_id"). + Joins("LEFT JOIN recordings r ON r.project_flock_kandangs_id = pfk.id"). + Joins("LEFT JOIN recording_eggs re ON re.recording_id = r.id"). + Where("pfk.id IN ?", projectFlockKandangIDs). + Where("pf.deleted_at IS NULL"). + Where("r.deleted_at IS NULL"). + Group("pfk.id, k.name"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// GetCumulativeEggProductionByProjectFlockID gets cumulative egg production for project flock +func (r *ClosingKeuanganRepositoryImpl) GetCumulativeEggProductionByProjectFlockID(ctx context.Context, projectFlockID uint) (int, float64, error) { + if projectFlockID == 0 { + return 0, 0, nil + } + + var result struct { + TotalQty float64 + TotalWeightKg float64 + } + + err := r.DB().WithContext(ctx). + Table("project_flocks pf"). + Select(` + COALESCE(SUM(re.qty), 0) AS total_qty, + COALESCE(SUM(re.qty * COALESCE(re.weight, 0)) / 1000, 0) AS total_weight_kg + `). + Joins("JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id"). + Joins("JOIN recordings r ON r.project_flock_kandangs_id = pfk.id"). + Joins("JOIN recording_eggs re ON re.recording_id = r.id"). + Where("pf.id = ?", projectFlockID). + Where("pf.deleted_at IS NULL"). + Where("r.deleted_at IS NULL"). + Where("r.record_datetime <= (SELECT MAX(record_datetime) FROM recordings WHERE project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID). + Scan(&result).Error + + if err != nil { + return 0, 0, err + } + + return int(result.TotalQty), result.TotalWeightKg, nil +} + +// === POPULATION QUERIES === + +// GetTotalPopulationByProjectFlockID gets total initial population for project flock +func (r *ClosingKeuanganRepositoryImpl) GetTotalPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(qty), 0)"). + Where("project_flock_id = ?", projectFlockID). + Scan(&result).Error + + return result, err +} + +// GetTotalPopulationByProjectFlockKandangIDs gets total population for multiple kandangs +func (r *ClosingKeuanganRepositoryImpl) GetTotalPopulationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(qty), 0)"). + Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). + Scan(&result).Error + + return result, err +} + +// GetRemainingPopulationByProjectFlockID gets remaining population based on depletion +func (r *ClosingKeuanganRepositoryImpl) GetRemainingPopulationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result struct { + TotalChickin float64 + TotalDepletion float64 + } + + err := r.DB().WithContext(ctx). + Table("project_flocks pf"). + Select(` + COALESCE((SELECT SUM(qty) FROM project_chickins WHERE project_flock_id = pf.id), 0) AS total_chickin, + COALESCE((SELECT SUM(COALESCE(rd.qty, 0)) + FROM recordings r + JOIN recording_depletions rd ON rd.recording_id = r.id + JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id + WHERE pfk.project_flock_id = pf.id), 0) AS total_depletion + `). + Where("pf.id = ?", projectFlockID). + Scan(&result).Error + + if err != nil { + return 0, err + } + + return result.TotalChickin - result.TotalDepletion, nil +} + +// === BUDGET QUERIES === + +// GetTotalBudgetByProjectFlockID gets total budget for project flock +func (r *ClosingKeuanganRepositoryImpl) GetTotalBudgetByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("project_budgets pb"). + Select("COALESCE(SUM(pb.amount), 0)"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = pb.project_flock_kandang_id"). + Where("pfk.project_flock_id = ?", projectFlockID). + Where("pb.deleted_at IS NULL"). + Scan(&result).Error + + return result, err +} + +// GetTotalBudgetByProjectFlockKandangIDs gets total budget for multiple kandangs +func (r *ClosingKeuanganRepositoryImpl) GetTotalBudgetByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("project_budgets pb"). + Select("COALESCE(SUM(pb.amount), 0)"). + Where("pb.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("pb.deleted_at IS NULL"). + Scan(&result).Error + + return result, err +} + +// === REALIZATION/EXPENSE QUERIES === + +// GetTotalRealizationByProjectFlockID gets total expense realization for project flock +func (r *ClosingKeuanganRepositoryImpl) GetTotalRealizationByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("expense_realizations er"). + Select("COALESCE(SUM(er.amount), 0)"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = er.project_flock_kandang_id"). + Where("pfk.project_flock_id = ?", projectFlockID). + Where("er.deleted_at IS NULL"). + Scan(&result).Error + + return result, err +} + +// GetTotalRealizationByProjectFlockKandangIDs gets total realization for multiple kandangs +func (r *ClosingKeuanganRepositoryImpl) GetTotalRealizationByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var result float64 + + err := r.DB().WithContext(ctx). + Table("expense_realizations er"). + Select("COALESCE(SUM(er.amount), 0)"). + Where("er.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("er.deleted_at IS NULL"). + Scan(&result).Error + + return result, err +} + +// GetActualUsageCostByProjectFlockID gets actual usage cost by project flock +func (r *ClosingKeuanganRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) { + if projectFlockID == 0 { + return []ActualUsageCostRow{}, nil + } + + db := r.DB().WithContext(ctx) + + // Get all project flock kandang IDs for this project flock + var pfkIDs []uint + err := db.Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Pluck("id", &pfkIDs).Error + if err != nil { + return nil, err + } + + if len(pfkIDs) == 0 { + return []ActualUsageCostRow{}, nil + } + + var rows []ActualUsageCostRow + + purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() + transferStockableKey := fifo.StockableKeyStockTransferIn.String() + + /* + RAW SQL FOR RECORDING QUERY (untuk pengecekan database): + + SELECT + pw.product_id, + p.name AS product_name, + COALESCE(f.name, tf.name) AS flag_name, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = '' THEN COALESCE(sa.qty, 0) + WHEN sa.stockable_type = '' THEN COALESCE(std.usage_qty, 0) + ELSE 0 + END + ), 0) AS total_qty, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = '' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) + WHEN sa.stockable_type = '' THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) + ELSE 0 + END + ), 0) AS total_price, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = '' THEN COALESCE(sa.qty, 0) + WHEN sa.stockable_type = '' THEN COALESCE(std.usage_qty, 0) + ELSE 0 + END + ), 0) / + NULLIF(COALESCE(SUM( + CASE + WHEN sa.stockable_type = '' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) + WHEN sa.stockable_type = '' THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) + ELSE 0 + END + ), 0), 0) AS average_price + FROM recordings r + JOIN recording_stocks rs ON rs.recording_id = r.id + JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id + JOIN products p ON p.id = pw.product_id + LEFT JOIN stock_allocations sa ON sa.usable_type = 'recording_stocks' AND sa.usable_id = rs.id AND sa.status = '' + LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = '' + LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = '' + LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id + LEFT JOIN purchase_items tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id + LEFT JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products' + LEFT JOIN flags tf ON tf.flagable_id = std.product_id AND tf.flagable_type = 'products' + WHERE r.project_flock_kandangs_id IN () + AND r.deleted_at IS NULL + GROUP BY pw.product_id, p.name, COALESCE(f.name, tf.name) + */ + + // Recording stock query (pakan, OVK, dll) dengan FIFO logic + recordingQuery := db. + Table("recordings AS r"). + Select(` + pw.product_id, + p.name AS product_name, + COALESCE(f.name, tf.name) AS flag_name, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) + WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) + ELSE 0 + END + ), 0) AS total_qty, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) + ELSE 0 + END + ), 0) AS total_price, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) + WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) + ELSE 0 + END + ), 0) AS qty_divisor, + COALESCE(SUM( + CASE + WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) + WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0) + ELSE 0 + END + ), 0) / NULLIF(COALESCE(SUM( + CASE + WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) + WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) + ELSE 0 + END + ), 0), 0) AS average_price`, + purchaseStockableKey, transferStockableKey, + purchaseStockableKey, transferStockableKey, + purchaseStockableKey, transferStockableKey, + purchaseStockableKey, transferStockableKey, + purchaseStockableKey, transferStockableKey). + Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). + Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). + Joins("JOIN products AS p ON p.id = pw.product_id"). + Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", + "recording_stocks", entity.StockAllocationStatusActive). + Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey). + Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey). + Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id"). + Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id"). + Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct). + Where("r.project_flock_kandangs_id IN ?", pfkIDs). + Where("r.deleted_at IS NULL"). + Group("pw.product_id, p.name, COALESCE(f.name, tf.name)") + + if err := recordingQuery.Scan(&rows).Error; err != nil { + return nil, err + } + + /* + RAW SQL FOR CHICKIN QUERY (untuk pengecekan database): + + SELECT + pw.product_id, + p.name AS product_name, + f.name AS flag_name, + COALESCE(SUM(pc.usage_qty), 0) AS total_qty, + COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price, + COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price + FROM project_chickins pc + JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id + JOIN products p ON p.id = pw.product_id + LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pc.product_warehouse_id + LEFT JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products' + WHERE pc.project_flock_kandang_id IN () + AND pc.usage_qty > 0 + GROUP BY pw.product_id, p.name, f.name + */ + + // Chickin query (DOC, pullet) dengan FIFO sederhana + chickinQuery := db. + Table("project_chickins AS pc"). + Select(` + pw.product_id, + p.name AS product_name, + f.name AS flag_name, + COALESCE(SUM(pc.usage_qty), 0) AS total_qty, + COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price, + COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price + `). + Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id"). + Joins("JOIN products AS p ON p.id = pw.product_id"). + Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id"). + Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("pc.project_flock_kandang_id IN ?", pfkIDs). + Where("pc.usage_qty > 0"). + Group("pw.product_id, p.name, f.name") + + var chickinRows []ActualUsageCostRow + if err := chickinQuery.Scan(&chickinRows).Error; err != nil { + return nil, err + } + + rows = append(rows, chickinRows...) + + return rows, nil +} + +// === EXPEDITION === + +// GetExpeditionHPP gets expedition HPP +func (r *ClosingKeuanganRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { + db := r.DB().WithContext(ctx) + + if projectFlockID == 0 { + return nil, fmt.Errorf("invalid project flock id") + } + + var results []ExpeditionHPPRow + + query := db. + Table("expense_realizations er"). + Select(` + s.name AS supplier_name, + COALESCE(SUM(er.amount), 0) AS total_amount + `). + Joins("JOIN suppliers s ON s.id = er.supplier_id"). + Where("er.category = 'EKSPEDISI'"). + Where("er.deleted_at IS NULL") + + if projectFlockKandangID != nil { + query = query.Where("er.project_flock_kandang_id = ?", *projectFlockKandangID) + } else { + query = query.Joins("JOIN project_flock_kandangs pfk ON pfk.id = er.project_flock_kandang_id"). + Where("pfk.project_flock_id = ?", projectFlockID) + } + + err := query. + Group("s.name"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// === PRODUCTS === + +// GetProductsWithFlagsByIDs gets products with their flags +func (r *ClosingKeuanganRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { + if len(productIDs) == 0 { + return []entity.Product{}, nil + } + + var products []entity.Product + + err := r.DB().WithContext(ctx). + Where("id IN ?", productIDs). + Preload("Flags", func(db *gorm.DB) *gorm.DB { + return db.Order("flagable_type, name") + }). + Find(&products).Error + + if err != nil { + return nil, err + } + + return products, nil +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 38529b0d..63424e61 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -48,6 +48,7 @@ type closingService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ClosingRepository + ClosingKeuanganRepo repository.ClosingKeuanganRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingRepo marketingRepository.MarketingRepository @@ -62,11 +63,12 @@ type closingService struct { ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, Repository: repo, + ClosingKeuanganRepo: closingKeuanganRepo, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingRepo: marketingRepo, @@ -578,6 +580,7 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl } func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { + s.Log.Infof("🔵 [CLOSING KEUANGAN] Starting fetch for ProjectFlockID: %d", projectFlockID) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, @@ -589,23 +592,35 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } + s.Log.Infof("✅ [CLOSING KEUANGAN] ProjectFlock fetched: ID=%d, Category=%s, FlockName=%s", + projectFlock.Id, projectFlock.Category, projectFlock.FlockName) + + // Validasi: Closing Keuangan hanya untuk LAYING, bukan GROWING + if projectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { + s.Log.Warnf("⚠️ [CLOSING KEUANGAN] ProjectFlock ID %d is GROWING category, closing keuangan not available", projectFlockID) + return nil, fiber.NewError(fiber.StatusNotFound, "Closing keuangan only available for LAYING 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") } + s.Log.Infof("💰 [CLOSING KEUANGAN] Budgets fetched: %d records", len(budgets)) - actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) + actualUsageRows, err := s.ClosingKeuanganRepo.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") } + s.Log.Infof("📊 [CLOSING KEUANGAN] Actual Usage Costs fetched: %d records", len(actualUsageRows)) purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) + s.Log.Infof("🛒 [CLOSING KEUANGAN] Converted to Purchase Items: %d items", len(purchaseItems)) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } + s.Log.Infof("💸 [CLOSING KEUANGAN] Expense Realizations fetched: %d records", len(realizations)) deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { return db.Preload("MarketingProduct"). @@ -615,26 +630,31 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } + s.Log.Infof("🚚 [CLOSING KEUANGAN] Marketing Delivery Products fetched: %d records", len(deliveryProducts)) chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } + s.Log.Infof("🐣 [CLOSING KEUANGAN] Chickins fetched: %d records", len(chickins)) totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } + s.Log.Infof("⚖️ [CLOSING KEUANGAN] Total Weight Produced: %.2f kg", totalWeightProduced) totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err) } + s.Log.Infof("🥚 [CLOSING KEUANGAN] Total Egg Weight: %.2f kg", totalEggWeightKg) totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } + s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion) input := dto.ClosingKeuanganInput{ ProjectFlockCategory: projectFlock.Category, @@ -650,6 +670,7 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* report := dto.ToClosingKeuanganReport(input) + s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID) return &report, nil } @@ -658,7 +679,7 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } - rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) + rows, err := s.ClosingKeuanganRepo.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP") @@ -1115,7 +1136,7 @@ func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, a } // Fetch products with flags from repository - products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs) + products, err := s.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs) if err != nil { s.Log.Warnf("Failed to fetch products for actual usage: %v", err) products = []entity.Product{}