Compare commits

...

3 Commits

5 changed files with 1152 additions and 342 deletions
@@ -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,
}
}
+2 -1
View File
@@ -25,6 +25,7 @@ type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db) closingRepo := rClosing.NewClosingRepository(db)
closingKeuanganRepo := rClosing.NewClosingKeuanganRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
@@ -40,7 +41,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) 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) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -10,7 +10,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" 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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -24,7 +23,7 @@ type ClosingRepository interface {
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, 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) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, 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) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, 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) 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 { type ClosingRepositoryImpl struct {
@@ -64,11 +61,6 @@ type SapronakRow struct {
Notes string `gorm:"column:notes"` Notes string `gorm:"column:notes"`
} }
type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"`
}
type SapronakQueryParams struct { type SapronakQueryParams struct {
Type string Type string
WarehouseIDs []uint WarehouseIDs []uint
@@ -127,219 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil 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 ( const (
sapronakIncomingPurchasesSQL = ` sapronakIncomingPurchasesSQL = `
SELECT SELECT
@@ -902,130 +681,180 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
return in, out, nil return in, out, nil
} }
type ActualUsageCostRow struct { // === CLOSING DATA PRODUKSI METHODS ===
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"` func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
FlagName string `gorm:"column:flag_name"` 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"` TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"` TotalPrice float64 `gorm:"column:total_price"`
AveragePrice float64 `gorm:"column:average_price"`
} }
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) { err := r.DB().WithContext(ctx).
if projectFlockID == 0 { Table("marketing_products mp").
return []ActualUsageCostRow{}, nil 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").
db := r.DB().WithContext(ctx) Scan(&agg).Error
// 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 { 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 nil, err
} }
if len(pfkIDs) == 0 { return standards, nil
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
} }
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
@@ -0,0 +1,773 @@
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)
// All Product Usage
GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]ProductUsageRow, 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"`
}
type ProductUsageRow struct {
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
FlagNames string `gorm:"column:flag_names"`
TotalQty float64 `gorm:"column:total_qty"`
Price float64 `gorm:"column:price"`
TotalPengeluaran float64 `gorm:"column:total_pengeluaran"`
}
// === 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 = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0
END
), 0) AS total_price,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' THEN COALESCE(std.usage_qty, 0)
ELSE 0
END
), 0) /
NULLIF(COALESCE(SUM(
CASE
WHEN sa.stockable_type = '<purchase_stockable_key>' THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = '<transfer_stockable_key>' 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 = '<active_status>'
LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = '<purchase_stockable_key>'
LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = '<transfer_stockable_key>'
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 (<pfk_ids>)
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 (<pfk_ids>)
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
}
// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang
// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments
func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]ProductUsageRow, error) {
if projectFlockKandangID == 0 {
return []ProductUsageRow{}, nil
}
var results []ProductUsageRow
rawQuery := `
SELECT
q.product_id,
q.product_name,
f.flag_names,
q.total_qty,
q.price,
q.total_qty * q.price AS total_pengeluaran
FROM (
SELECT
product_id,
product_name,
SUM(total_qty) AS total_qty,
AVG(price) AS price
FROM (
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(
CASE
WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = 'STOCK_TRANSFER_IN' THEN COALESCE(std.usage_qty, 0)
WHEN sa.stockable_type = 'TRANSFERTOLAYING_IN' THEN COALESCE(ltt.total_used, 0)
WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN COALESCE(adjs.total_used, 0)
WHEN sa.stockable_type = 'PROJECT_FLOCK_POPULATION' THEN COALESCE(pfp.total_used_qty, 0)
ELSE 0
END
), 0) AS total_qty,
COALESCE(AVG(CASE WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN COALESCE(pi.price, 0) END), 0) AS 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_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'
LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'
LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'
LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'
LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'
LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
WHERE r.project_flock_kandangs_id = ?
AND r.deleted_at IS NULL
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS 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 = pw.id
WHERE pc.project_flock_kandang_id = ?
AND pc.usage_qty > 0
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(mdp.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM marketing_delivery_products mdp
JOIN marketing_products mp ON mp.id = mdp.marketing_product_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(lts.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM laying_transfer_sources lts
JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id
JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(std.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM stock_transfer_details std
JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id
JOIN products p ON p.id = std.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
GROUP BY pw.product_id, p.name
UNION ALL
SELECT
pw.product_id,
p.name AS product_name,
COALESCE(SUM(ads.usage_qty), 0) AS total_qty,
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS price
FROM adjustment_stocks ads
JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id
JOIN products p ON p.id = pw.product_id
LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id
WHERE pw.project_flock_kandang_id = ?
AND ads.usage_qty > 0
GROUP BY pw.product_id, p.name
) x
GROUP BY product_id, product_name
) q
LEFT JOIN (
SELECT
p.id AS product_id,
STRING_AGG(DISTINCT f.name, ', ') AS flag_names
FROM products p
LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id
GROUP BY p.id
) f ON f.product_id = q.product_id
ORDER BY q.product_name
`
err := r.DB().WithContext(ctx).
Raw(rawQuery, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID, projectFlockKandangID).
Scan(&results).Error
if err != nil {
return nil, fmt.Errorf("failed to get all product usage: %w", err)
}
return results, nil
}
@@ -48,6 +48,7 @@ type closingService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.ClosingRepository Repository repository.ClosingRepository
ClosingKeuanganRepo repository.ClosingKeuanganRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository MarketingRepo marketingRepository.MarketingRepository
@@ -62,11 +63,12 @@ type closingService struct {
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository 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{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ClosingKeuanganRepo: closingKeuanganRepo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo, 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) { 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(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, 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 { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") 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) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") 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 { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") 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) 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) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") 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 { deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("MarketingProduct"). 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) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") 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) chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") 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) totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) 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) totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err) 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) totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
} }
s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion)
input := dto.ClosingKeuanganInput{ input := dto.ClosingKeuanganInput{
ProjectFlockCategory: projectFlock.Category, ProjectFlockCategory: projectFlock.Category,
@@ -650,6 +670,7 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
report := dto.ToClosingKeuanganReport(input) report := dto.ToClosingKeuanganReport(input)
s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID)
return &report, nil 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") 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 { if err != nil {
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) 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") 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 // Fetch products with flags from repository
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs) products, err := s.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs)
if err != nil { if err != nil {
s.Log.Warnf("Failed to fetch products for actual usage: %v", err) s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
products = []entity.Product{} products = []entity.Product{}