diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 6ca19d5c..a7238b17 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,8 +1,12 @@ package dto -// === CLOSING KEUANGAN CODES === +import ( + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) -// Closing HPP Codes type ClosingHPPCode string const ( @@ -14,36 +18,30 @@ const ( HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI" ) -// Closing Profit Loss Codes type ClosingProfitLossCode string const ( - PLCodeSales ClosingProfitLossCode = "SALES" - PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" - PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" + PLCodeSales ClosingProfitLossCode = "SALES" + PLCodeSapronak ClosingProfitLossCode = "SAPRONAK" + PLCodeOverhead ClosingProfitLossCode = "OVERHEAD" PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI" ) -// === NEW CLOSING KEUANGAN DTO === - -// FinancialMetrics represents financial metrics with per unit and total amounts type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` Amount float64 `json:"amount"` } -// HPPItem represents an item in HPP section type HPPItem struct { ID uint `json:"id"` - Category string `json:"category"` // "purchase" or "overhead" - Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI" + Category string `json:"category"` + Code string `json:"code"` Label string `json:"label"` Budgeting FinancialMetrics `json:"budgeting"` Realization FinancialMetrics `json:"realization"` } -// HPPSummary represents summary for HPP section type HPPSummary struct { Label string `json:"label"` Budgeting FinancialMetrics `json:"budgeting"` @@ -52,52 +50,41 @@ type HPPSummary struct { EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` } -// HPPSection represents HPP data section type HPPSection struct { - Items []HPPItem `json:"items"` + Items []HPPItem `json:"items"` Summary HPPSummary `json:"summary"` } -// ProfitLossItem represents an item in Profit & Loss section type ProfitLossItem struct { - Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI" + Code string `json:"code"` Label string `json:"label"` - Type string `json:"type"` // "income", "purchase", "overhead" + Type string `json:"type"` RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` Amount float64 `json:"amount"` } -// ProfitLossSummary represents summary for Profit & Loss section type ProfitLossSummary struct { GrossProfit FinancialMetrics `json:"gross_profit"` SubTotal FinancialMetrics `json:"sub_total"` NetProfit FinancialMetrics `json:"net_profit"` } -// ProfitLossSection represents Profit & Loss data section type ProfitLossSection struct { - Items []ProfitLossItem `json:"items"` - Summary ProfitLossSummary `json:"summary"` + Items []ProfitLossItem `json:"items"` + Summary ProfitLossSummary `json:"summary"` } -// ClosingKeuanganData represents the main data structure type ClosingKeuanganData struct { - HPP HPPSection `json:"hpp"` + HPP HPPSection `json:"hpp"` ProfitLoss ProfitLossSection `json:"profit_loss"` } - -// ClosingKeuanganResponse represents the full API response -type ClosingKeuanganResponse struct { - Code int `json:"code"` - Status string `json:"status"` - Message string `json:"message"` - Data ClosingKeuanganData `json:"data"` +type MetricsCalculator struct { + TotalPopulation float64 + ActualPopulation float64 + TotalWeightProduced float64 } -// === MAPPER FUNCTIONS === - -// ToFinancialMetrics creates FinancialMetrics from values func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { return FinancialMetrics{ RpPerBird: rpPerBird, @@ -106,7 +93,6 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { } } -// ToHPPItem creates HPP item func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem { return HPPItem{ ID: id, @@ -118,7 +104,6 @@ func ToHPPItem(id uint, category, code, label string, budgeting, realization Fin } } -// ToHPPSummary creates HPP summary func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary { return HPPSummary{ Label: label, @@ -129,7 +114,6 @@ func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudg } } -// ToHPPSection creates HPP section func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { return HPPSection{ Items: items, @@ -137,7 +121,6 @@ func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection { } } -// ToProfitLossItem creates Profit & Loss item func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem { return ProfitLossItem{ Code: code, @@ -149,7 +132,6 @@ func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount f } } -// ToProfitLossSummary creates Profit & Loss summary func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary { return ProfitLossSummary{ GrossProfit: grossProfit, @@ -158,7 +140,6 @@ func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) Prof } } -// ToProfitLossSection creates Profit & Loss section func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection { return ProfitLossSection{ Items: items, @@ -166,7 +147,6 @@ func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) Prof } } -// ToClosingKeuanganData creates complete closing keuangan data func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData { return ClosingKeuanganData{ HPP: hpp, @@ -174,12 +154,72 @@ func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) Closing } } -// ToSuccessClosingKeuanganResponse creates success response -func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse { - return ClosingKeuanganResponse{ - Code: 200, - Status: "success", - Message: "Get closing keuangan successfully", - Data: data, +func (mc *MetricsCalculator) CalculateMetrics(amount float64) (rpPerBird, rpPerKg float64) { + if mc.ActualPopulation > 0 { + rpPerBird = amount / mc.ActualPopulation } + if mc.TotalWeightProduced > 0 { + rpPerKg = amount / mc.TotalWeightProduced + } + return +} + +func (mc *MetricsCalculator) CalculateProfitLossMetrics(amount float64) (rpPerBird, rpPerKg float64) { + if mc.TotalPopulation > 0 { + rpPerBird = amount / mc.TotalPopulation + } + if mc.TotalWeightProduced > 0 { + rpPerKg = amount / mc.TotalWeightProduced + } + return +} + +type ProductFilter struct { + ProjectFlockCategory string +} + +func (pf *ProductFilter) IsEggProduct(product entity.Product) bool { + for _, flag := range product.Flags { + flagName := strings.ToUpper(flag.Name) + if flagName == string(utils.FlagTelur) || + flagName == string(utils.FlagTelurUtuh) || + flagName == string(utils.FlagTelurPecah) || + flagName == string(utils.FlagTelurPutih) || + flagName == string(utils.FlagTelurRetak) { + return true + } + } + return false +} + +func (pf *ProductFilter) IsChickenProduct(product entity.Product) bool { + for _, flag := range product.Flags { + flagName := strings.ToUpper(flag.Name) + if flagName == string(utils.FlagAyamAfkir) || + flagName == string(utils.FlagAyamCulling) || + flagName == string(utils.FlagAyamMati) { + return true + } + } + return false +} + +func (pf *ProductFilter) ShouldIncludeProduct(product entity.Product) bool { + if pf.ProjectFlockCategory == string(utils.ProjectFlockCategoryLaying) { + return pf.IsEggProduct(product) + } + return pf.IsChickenProduct(product) || (!pf.IsEggProduct(product) && !pf.IsChickenProduct(product)) +} + +func (pf *ProductFilter) FilterDeliveryProducts(deliveries []entity.MarketingDeliveryProduct) []entity.MarketingDeliveryProduct { + filtered := make([]entity.MarketingDeliveryProduct, 0) + for _, delivery := range deliveries { + if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { + continue + } + if pf.ShouldIncludeProduct(delivery.MarketingProduct.ProductWarehouse.Product) { + filtered = append(filtered, delivery) + } + } + return filtered } diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index 1079663d..666c055d 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -25,7 +25,6 @@ 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,9 +39,11 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) + hppCostRepo := commonRepo.NewHppCostRepository(db) + hppService := commonSvc.NewHppService(hppCostRepo) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) - closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo) + closingKeuanganService := sClosing.NewClosingKeuanganService(projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo, hppService, hppCostRepo) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/repositories/closingKeuangan.repository.go b/internal/modules/closings/repositories/closingKeuangan.repository.go deleted file mode 100644 index dedea807..00000000 --- a/internal/modules/closings/repositories/closingKeuangan.repository.go +++ /dev/null @@ -1,365 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "sort" - "strings" - - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -// ClosingKeuanganRepository handles database operations for closing keuangan -type ClosingKeuanganRepository interface { - repository.BaseRepository[interface{}] - - // All Product Usage - GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) - - // Depletion per kandang - GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) - - // Weight produced from uniformity per kandang - GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) - - // DB returns the underlying GORM DB instance - DB() *gorm.DB -} - -type ClosingKeuanganRepositoryImpl struct { - *repository.BaseRepositoryImpl[interface{}] -} - -func NewClosingKeuanganRepository(db *gorm.DB) ClosingKeuanganRepository { - return &ClosingKeuanganRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[interface{}](db), - } -} - -// Result Rows - -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"` -} - -// GetAllProductUsageByProjectFlockKandangID gets all product usage for a project flock kandang -// Combines data from all usable types: recordings, chickins, marketing, transfers, adjustments -// flagFilters: optional filter to get only specific flags (e.g., ["PAKAN", "OVK"]), empty means get all -func (r *ClosingKeuanganRepositoryImpl) GetAllProductUsageByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, flagFilters []string) ([]ProductUsageRow, error) { - if projectFlockKandangID == 0 { - return []ProductUsageRow{}, nil - } - - type SubQueryResult struct { - ProductID uint `gorm:"column:product_id"` - ProductName string `gorm:"column:product_name"` - TotalQty float64 `gorm:"column:total_qty"` - Price float64 `gorm:"column:price"` - } - - type AggregatedResult struct { - ProductID uint `gorm:"column:product_id"` - ProductName string `gorm:"column:product_name"` - TotalQty float64 `gorm:"column:total_qty"` - Price float64 `gorm:"column:price"` - PriceCount int `gorm:"-"` // For calculating average price - } - - type FlagResult struct { - ProductID uint `gorm:"column:product_id"` - FlagNames string `gorm:"column:flag_names"` - } - - var allResults []SubQueryResult - - // Subquery 1: Recordings - var recordingsResults []SubQueryResult - err := r.DB().WithContext(ctx). - Table("recordings r"). - 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"). - Joins("JOIN recording_stocks rs ON rs.recording_id = r.id"). - Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN stock_allocations sa ON sa.usable_type = 'RECORDING_STOCK' AND sa.usable_id = rs.id AND sa.status = 'ACTIVE'"). - Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'"). - Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = 'STOCK_TRANSFER_IN'"). - Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = 'TRANSFERTOLAYING_IN'"). - Joins("LEFT JOIN adjustment_stocks adjs ON adjs.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'"). - Joins("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 = ?", projectFlockKandangID). - Where("r.deleted_at IS NULL"). - Group("pw.product_id, p.name"). - Scan(&recordingsResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get recordings product usage: %w", err) - } - fmt.Printf("[REPO] Recordings query: %d results for projectFlockKandangID=%d\n", len(recordingsResults), projectFlockKandangID) - allResults = append(allResults, recordingsResults...) - - // Subquery 2: Chickins - var chickinsResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("project_chickins pc"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = pc.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pc.project_flock_kandang_id = ?", projectFlockKandangID). - Where("pc.usage_qty > 0"). - Group("pw.product_id, p.name"). - Scan(&chickinsResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get chickins product usage: %w", err) - } - fmt.Printf("[REPO] Chickins query: %d results for projectFlockKandangID=%d\n", len(chickinsResults), projectFlockKandangID) - allResults = append(allResults, chickinsResults...) - - // Subquery 3: Marketing Delivery - var marketingResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("marketing_delivery_products mdp"). - 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"). - Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). - Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&marketingResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get marketing product usage: %w", err) - } - fmt.Printf("[REPO] Marketing query: %d results for projectFlockKandangID=%d\n", len(marketingResults), projectFlockKandangID) - allResults = append(allResults, marketingResults...) - - // Subquery 4: Laying Transfer Sources - var layingTransferResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("laying_transfer_sources lts"). - 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"). - Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id"). - Joins("JOIN product_warehouses pw ON pw.id = lts.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&layingTransferResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get laying transfer product usage: %w", err) - } - fmt.Printf("[REPO] Laying Transfer query: %d results for projectFlockKandangID=%d\n", len(layingTransferResults), projectFlockKandangID) - allResults = append(allResults, layingTransferResults...) - - // Subquery 5: Stock Transfer Details - var stockTransferResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("stock_transfer_details std"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = std.source_product_warehouse_id"). - Joins("JOIN products p ON p.id = std.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Group("pw.product_id, p.name"). - Scan(&stockTransferResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get stock transfer product usage: %w", err) - } - fmt.Printf("[REPO] Stock Transfer query: %d results for projectFlockKandangID=%d\n", len(stockTransferResults), projectFlockKandangID) - allResults = append(allResults, stockTransferResults...) - - // Subquery 6: Adjustment Stocks - var adjustmentResults []SubQueryResult - err = r.DB().WithContext(ctx). - Table("adjustment_stocks ads"). - 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"). - Joins("JOIN product_warehouses pw ON pw.id = ads.product_warehouse_id"). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). - Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). - Where("ads.usage_qty > 0"). - Group("pw.product_id, p.name"). - Scan(&adjustmentResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get adjustment product usage: %w", err) - } - fmt.Printf("[REPO] Adjustment query: %d results for projectFlockKandangID=%d\n", len(adjustmentResults), projectFlockKandangID) - allResults = append(allResults, adjustmentResults...) - - fmt.Printf("[REPO] Total raw results before aggregation: %d items\n", len(allResults)) - - // Aggregate results by product_id - aggregatedMap := make(map[uint]*AggregatedResult) - for _, result := range allResults { - key := result.ProductID - if existing, exists := aggregatedMap[key]; exists { - existing.TotalQty += result.TotalQty - existing.Price += result.Price - existing.PriceCount++ - } else { - aggregatedMap[key] = &AggregatedResult{ - ProductID: result.ProductID, - ProductName: result.ProductName, - TotalQty: result.TotalQty, - Price: result.Price, - PriceCount: 1, - } - } - } - - fmt.Printf("[REPO] Aggregated to %d unique products\n", len(aggregatedMap)) - - // Get flags for all products - productIDs := make([]uint, 0, len(aggregatedMap)) - for id := range aggregatedMap { - productIDs = append(productIDs, id) - } - - var flagResults []FlagResult - if len(productIDs) > 0 { - err = r.DB().WithContext(ctx). - Table("products p"). - Select("p.id as product_id, STRING_AGG(DISTINCT f.name, ', ') as flag_names"). - Joins("LEFT JOIN flags f ON f.flagable_type = 'products' AND f.flagable_id = p.id"). - Where("p.id IN ?", productIDs). - Group("p.id"). - Scan(&flagResults).Error - - if err != nil { - return nil, fmt.Errorf("failed to get product flags: %w", err) - } - } - fmt.Printf("[REPO] Fetched flags for %d products\n", len(flagResults)) - - // Build flag map - flagMap := make(map[uint]string) - for _, flag := range flagResults { - flagMap[flag.ProductID] = flag.FlagNames - } - - // Combine results and calculate average price - results := make([]ProductUsageRow, 0, len(aggregatedMap)) - for _, agg := range aggregatedMap { - avgPrice := float64(0) - if agg.PriceCount > 0 { - avgPrice = agg.Price / float64(agg.PriceCount) - } - - flagNames := flagMap[agg.ProductID] - - // Apply flag filters if provided - if len(flagFilters) > 0 { - // Check if any of the flagFilters exist in flagNames - matched := false - for _, filter := range flagFilters { - if containsIgnoreCase(flagNames, filter) { - matched = true - break - } - } - if !matched { - continue // Skip this product if no flag matches - } - } - - results = append(results, ProductUsageRow{ - ProductID: agg.ProductID, - ProductName: agg.ProductName, - FlagNames: flagNames, - TotalQty: agg.TotalQty, - Price: avgPrice, - TotalPengeluaran: agg.TotalQty * avgPrice, - }) - } - - fmt.Printf("[REPO] After filtering with flagFilters=%v: %d results\n", flagFilters, len(results)) - for i, r := range results { - fmt.Printf("[REPO] Result[%d]: ProductID=%d, ProductName=%s, FlagNames=%s, TotalQty=%.2f, Price=%.2f, TotalPengeluaran=%.2f\n", - i, r.ProductID, r.ProductName, r.FlagNames, r.TotalQty, r.Price, r.TotalPengeluaran) - } - - // Sort by product name - sort.Slice(results, func(i, j int) bool { - return results[i].ProductName < results[j].ProductName - }) - - fmt.Printf("[REPO] Final sorted results: %d items\n", len(results)) - return results, nil -} - -// GetTotalDepletionByProjectFlockKandangID gets total depletion for a specific kandang -func (r *ClosingKeuanganRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { - var result float64 - err := r.DB().WithContext(ctx). - Table("recording_depletions"). - Select("COALESCE(SUM(recording_depletions.qty), 0)"). - Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). - Where("project_flock_kandangs.id = ?", projectFlockKandangID). - Scan(&result).Error - return result, err -} - -// GetTotalWeightProducedFromUniformityByProjectFlockKandangID calculates total weight produced from uniformity data for a specific kandang -// Formula: (mean_up / 1.10) * chick_qty_of_weight / 1000 -func (r *ClosingKeuanganRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { - if projectFlockKandangID == 0 { - return 0, nil - } - - var uniformity struct { - MeanUp float64 - ChickQtyOfWeight float64 - } - - err := r.DB().WithContext(ctx). - Table("project_flock_kandang_uniformity"). - Select("mean_up, chick_qty_of_weight"). - Where("project_flock_kandang_id = ?", projectFlockKandangID). - Order("id DESC"). - Limit(1). - Scan(&uniformity).Error - - if err != nil { - return 0, err - } - - // Calculate weight: (mean_up / 1.10) * chick_qty_of_weight / 1000 - totalWeight := (uniformity.MeanUp / 1.10) * uniformity.ChickQtyOfWeight / 1000 - - return totalWeight, nil -} - -// containsIgnoreCase checks if a string contains a substring (case-insensitive) -func containsIgnoreCase(str, substr string) bool { - return strings.Contains(strings.ToUpper(str), strings.ToUpper(substr)) -} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 02942f44..5494a835 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -162,7 +162,12 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { - realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) + if err != nil { + return nil, err + } + + realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID, projectFlock.Category) if err != nil { return nil, err } diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index 0f3351f7..85aa5f1c 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -2,20 +2,19 @@ package service import ( "errors" - "strings" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" - repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "github.com/gofiber/fiber/v2" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -25,9 +24,28 @@ type ClosingKeuanganService interface { GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) } +// CostData holds all cost-related information +type CostData struct { + FeedCost float64 + OvkCost float64 + ChickenCost float64 + ExpeditionCost float64 + BudgetOperational float64 + RealizationOperational float64 +} + +// ProductionData holds all production and sales related information +type ProductionData struct { + TotalPopulationIn float64 + TotalDepletion float64 + TotalWeightProduced float64 + TotalEggWeightKg float64 + TotalWeightSold float64 + TotalSalesAmount float64 +} + type closingKeuanganService struct { Log *logrus.Logger - ClosingKeuanganRepo repository.ClosingKeuanganRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository @@ -35,10 +53,11 @@ type closingKeuanganService struct { ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository RecordingRepo recordingRepository.RecordingRepository + HppSvc commonSvc.HppService + HppRepo commonRepo.HppCostRepository } func NewClosingKeuanganService( - closingKeuanganRepo repository.ClosingKeuanganRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, @@ -46,10 +65,11 @@ func NewClosingKeuanganService( projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, recordingRepo recordingRepository.RecordingRepository, + hppSvc commonSvc.HppService, + hppRepo commonRepo.HppCostRepository, ) ClosingKeuanganService { return &closingKeuanganService{ Log: utils.Log, - ClosingKeuanganRepo: closingKeuanganRepo, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, @@ -57,6 +77,8 @@ func NewClosingKeuanganService( ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, + HppSvc: hppSvc, + HppRepo: hppRepo, } } @@ -73,30 +95,12 @@ func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") - } - - // Preload Nonstock.Flags manually - var budgetIDs []uint - for _, b := range budgets { - budgetIDs = append(budgetIDs, b.Id) - } - if len(budgetIDs) > 0 { - err = s.ProjectBudgetRepo.DB().WithContext(c.Context()). - Preload("Nonstock.Flags"). - Where("id IN ?", budgetIDs). - Find(&budgets).Error - } - - // Get all kandang for this project flock - kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) + projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } - return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) + return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) } func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) { @@ -107,12 +111,11 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec return nil, err } - // Validate and fetch project flock kandang - kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") } - if kandang.ProjectFlockId != projectFlockID { + if projectFlockKandang.ProjectFlockId != projectFlockID { return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock") } @@ -121,417 +124,249 @@ func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projec return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") - } + projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang} - // Preload Nonstock.Flags manually - var budgetIDs []uint - for _, b := range budgets { - budgetIDs = append(budgetIDs, b.Id) - } - if len(budgetIDs) > 0 { - err = s.ProjectBudgetRepo.DB().WithContext(c.Context()). - Preload("Nonstock.Flags"). - Where("id IN ?", budgetIDs). - Find(&budgets).Error - } - - kandangs := []entity.ProjectFlockKandang{*kandang} - - return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID) + return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs) } -func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) { - // Define flag filters using constants - pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)} - ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)} - ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)} - allFilters := append(pakanFilters, ovkFilters...) - allFilters = append(allFilters, ayamFilters...) +func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang) (*dto.ClosingKeuanganData, error) { - var allProductUsageRows []repository.ProductUsageRow - - // Get ALL product usage - for _, kandang := range kandangs { - rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters) - if err == nil { - allProductUsageRows = append(allProductUsageRows, rows...) - } + var projectFlockKandangIDs []uint + for _, projectFlockKandang := range projectFlockKandangs { + projectFlockKandangIDs = append(projectFlockKandangIDs, projectFlockKandang.Id) } - // Classify into categories based on flag priority - var pakanProductUsageRows []repository.ProductUsageRow - var ovkProductUsageRows []repository.ProductUsageRow - var ayamProductUsageRows []repository.ProductUsageRow - - for _, row := range allProductUsageRows { - // Parse flag names from comma-separated string - flagNames := strings.Split(row.FlagNames, ",") - - hasPakanFlag := false - hasOvkFlag := false - hasAyamFlag := false - - for _, flag := range flagNames { - flag = strings.TrimSpace(flag) - if containsItem(pakanFilters, flag) { - hasPakanFlag = true - } - if containsItem(ovkFilters, flag) { - hasOvkFlag = true - } - if containsItem(ayamFilters, flag) { - hasAyamFlag = true - } - } - - // Priority: PAKAN > OVK > AYAM - if hasPakanFlag { - pakanProductUsageRows = append(pakanProductUsageRows, row) - } else if hasOvkFlag { - ovkProductUsageRows = append(ovkProductUsageRows, row) - } else if hasAyamFlag { - ayamProductUsageRows = append(ayamProductUsageRows, row) - } else { - continue - } - } - - - // Calculate total price for each category - var totalPakanPrice, totalOvkPrice, totalAyamPrice float64 - for _, row := range pakanProductUsageRows { - totalPakanPrice += row.TotalPengeluaran - } - for _, row := range ovkProductUsageRows { - totalOvkPrice += row.TotalPengeluaran - } - for _, row := range ayamProductUsageRows { - totalAyamPrice += row.TotalPengeluaran - } - - // Determine if this is per-kandang or per-project-flock scope - isPerKandang := len(kandangs) == 1 + isPerKandang := len(projectFlockKandangs) == 1 var projectFlockKandangID *uint if isPerKandang { - kandangID := kandangs[0].Id + kandangID := projectFlockKandangs[0].Id projectFlockKandangID = &kandangID } + costs, err := s.calculateCosts(c, projectFlock, projectFlockKandangs, projectFlockKandangIDs, projectFlockKandangID) + if err != nil { + return nil, err + } + + productionData, err := s.calculateProductionData(c, projectFlock, projectFlockKandangIDs, projectFlockKandangID) + if err != nil { + return nil, err + } + + hppSection := s.buildHPPSection(c, projectFlock, projectFlockKandangs, costs, productionData) + + profitLossSection := s.buildProfitLossSection(projectFlock, costs, productionData) + + data := dto.ToClosingKeuanganData(hppSection, profitLossSection) + return &data, nil +} + +func (s closingKeuanganService) calculateCosts(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*CostData, error) { + costs := &CostData{} var err error - // Fetch realizations - var realizations []entity.ExpenseRealization - if isPerKandang && projectFlockKandangID != nil { - realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID) - } else { - realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil) - } + costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") + costs.FeedCost = 0 } - deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB { - db = db.Preload("MarketingProduct"). - Preload("MarketingProduct.ProductWarehouse"). - Preload("MarketingProduct.ProductWarehouse.Product") - return db - }) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") - } - - // Filter by kandang if scope is per-kandang (manual filtering after fetch) - if isPerKandang && projectFlockKandangID != nil { - filteredProducts := make([]entity.MarketingDeliveryProduct, 0) - for _, dp := range deliveryProducts { - pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId - if pfKandangID != nil && *pfKandangID == *projectFlockKandangID { - filteredProducts = append(filteredProducts, dp) - } - } - deliveryProducts = filteredProducts - } - - // Fetch chickins - var chickins []entity.ProjectChickin - if isPerKandang && projectFlockKandangID != nil { - chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) - } + costs.OvkCost, err = s.HppRepo.GetOvkUsageCost(c.Context(), projectFlockKandangIDs, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") + costs.OvkCost = 0 } - // Get total depletion - var totalDepletion float64 - if isPerKandang && projectFlockKandangID != nil { - totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) - } - if err != nil { - totalDepletion = 0 - } - - totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id) - if err != nil { - } - - // Try to get actual weight from uniformity data - var totalWeightFromUniformity float64 - if isPerKandang && projectFlockKandangID != nil { - totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) - } else { - totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) - } - if err != nil { - } else if totalWeightFromUniformity > 0 { - totalWeightProduced = totalWeightFromUniformity - } - - // Fetch egg data only for Laying category - var totalEggWeightKg float64 if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - // TODO: Replace with actual method to get egg weight from RecordingRepo - // totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id) - // For now, set to 0 as placeholder - totalEggWeightKg = 0 + for _, projectFlockKandang := range projectFlockKandangs { + depresiasiCost, err := s.HppSvc.GetDepresiasiTransfer(projectFlockKandang.Id, nil) + if err == nil { + costs.ChickenCost += depresiasiCost + } + pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) + if err == nil { + costs.ChickenCost += pulletCost + } + } } else { - totalEggWeightKg = 0 + for _, projectFlockKandang := range projectFlockKandangs { + pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id) + if err == nil { + costs.ChickenCost += pulletCost + } + } } - // Build new DTO structure - - // Calculate totals - var totalPopulation float64 - for _, chickin := range chickins { - totalPopulation += chickin.UsageQty + costs.ExpeditionCost, err = s.HppRepo.GetExpedisionCost(c.Context(), projectFlockKandangIDs) + if err != nil { + costs.ExpeditionCost = 0 } - // Calculate actual population (total population - depletion) - actualPopulation := totalPopulation - totalDepletion - - // Calculate budget totals by category - calculateBudgetByFlag := func(flags []string) float64 { - var total float64 + if budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlock.Id); err == nil { + totalBudget := 0.0 for _, budget := range budgets { - if budget.Nonstock != nil { - for _, nonstockFlag := range budget.Nonstock.Flags { - flagName := strings.ToUpper(nonstockFlag.Name) - for _, targetFlag := range flags { - if flagName == strings.ToUpper(targetFlag) { - total += budget.Price * budget.Qty - break - } - } - } + totalBudget += budget.Price * budget.Qty + } + if projectFlockKandangID != nil { + allKandangs, errKandang := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlock.Id) + if errKandang == nil && len(allKandangs) > 0 { + costs.BudgetOperational = totalBudget / float64(len(allKandangs)) + } + } else { + costs.BudgetOperational = totalBudget + } + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch budgets for project_flock_id=%d: %+v", projectFlock.Id, err) + } + + if realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID); err == nil { + for _, realization := range realizations { + amount := realization.Price * realization.Qty + isEkspedisi := realization.ExpenseNonstock != nil && + realization.ExpenseNonstock.Nonstock != nil && + containsFlag(realization.ExpenseNonstock.Nonstock.Flags, "EKSPEDISI") + if !isEkspedisi { + costs.RealizationOperational += amount } } - return total + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch realizations for project_flock_id=%d: %+v", projectFlock.Id, err) } - // Budget per category - budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}) - budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"}) - budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"}) - budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"}) + return costs, nil +} - // Operational budget = total budget - pakan - ovk - ayam - ekspedisi - totalBudgetAmount := 0.0 - for _, budget := range budgets { - totalBudgetAmount += budget.Price * budget.Qty +func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*ProductionData, error) { + data := &ProductionData{} + var err error + + data.TotalPopulationIn, err = s.HppRepo.GetTotalPopulation(c.Context(), projectFlockKandangIDs) + if err != nil { + data.TotalPopulationIn = 0 } - budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi + if projectFlockKandangID != nil { + data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID) + } else { + data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id) + } + if err != nil { + data.TotalDepletion = 0 + } - // Calculate realization totals - var totalRealizationAmount float64 - var totalEkspedisiRealization float64 - for _, realization := range realizations { - amount := realization.Price * realization.Qty - totalRealizationAmount += amount + if projectFlockKandangID != nil { + data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID) + } else { + data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id) + } + if err != nil { + data.TotalWeightProduced = 0 + } - // Check if this is ekspedisi (need to check nonstock flags) - if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil { - for _, flag := range realization.ExpenseNonstock.Nonstock.Flags { - if flag.Name == "EKSPEDISI" { - totalEkspedisiRealization += amount - break - } - } + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil) + if err != nil { + data.TotalEggWeightKg = 0 } } - totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization + var deliveryProducts []entity.MarketingDeliveryProduct + if projectFlockKandangID != nil { + deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category) + } else { + deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, nil, projectFlock.Category) + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan") + } - // Filter delivery products based on category - var filteredDeliveryProducts []entity.MarketingDeliveryProduct for _, delivery := range deliveryProducts { - // Get product from delivery if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 { continue } - - product := delivery.MarketingProduct.ProductWarehouse.Product - isEggProduct := false - isChickenProduct := false - - // Check product flags - for _, flag := range product.Flags { - flagName := strings.ToUpper(flag.Name) - - // Egg product flags - if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" || - flagName == "TELURPUTIH" || flagName == "TELURRETAK" { - isEggProduct = true - } - - // Chicken product flags - if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" { - isChickenProduct = true - } - } - - // Filter based on project flock category - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - // Laying: only egg products - if isEggProduct { - filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) - } - } else { - // Growing/Contract Growing: only chicken products - if isChickenProduct || (!isEggProduct && !isChickenProduct) { - // Include if chicken product or if no specific flags (default to chicken) - filteredDeliveryProducts = append(filteredDeliveryProducts, delivery) - } - } + data.TotalWeightSold += delivery.TotalWeight + data.TotalSalesAmount += delivery.TotalPrice } + return data, nil +} - // Calculate total weight sold and sales amount from filtered products - var totalWeightSold float64 - var totalSalesAmount float64 - for _, delivery := range filteredDeliveryProducts { - totalWeightSold += delivery.TotalWeight - totalSalesAmount += delivery.TotalPrice +func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection { + + actualPopulation := production.TotalPopulationIn - production.TotalDepletion + totalWeightProduced := production.TotalWeightProduced + totalEggWeightKg := production.TotalEggWeightKg + + weightForCalculation := totalWeightProduced + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + weightForCalculation = totalEggWeightKg } - - // Calculate metrics - always use kg ayam for rp_per_kg calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { - rpPerBird = amount / actualPopulation // Use actual population + rpPerBird = amount / actualPopulation } - if totalWeightProduced > 0 { - rpPerKg = amount / totalWeightProduced + if weightForCalculation > 0 { + rpPerKg = amount / weightForCalculation } return } - // Calculate metrics for profit loss (use total population and total weight produced) - calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { - if totalPopulation > 0 { - rpPerBird = amount / totalPopulation - } - if totalWeightProduced > 0 { - rpPerKg = amount / totalWeightProduced - } - return + createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem { + budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount) + realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount) + return dto.ToHPPItem( + id, + category, + code, + label, + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ) } - // Build HPP Items using constants hppItems := []dto.HPPItem{} - // PAKAN item - pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan) - pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 1, - "purchase", - string(dto.HPPCodePakan), - "Pembelian Pakan", - dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan), - dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice), - )) + hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost)) + hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost)) - // OVK item - ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk) - ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 2, - "purchase", - string(dto.HPPCodeOVK), - "Pembelian OVK", - dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk), - dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice), - )) - - // DOC/DEPRESIASI item docCode := string(dto.HPPCodeDOC) docLabel := "Pembelian DOC" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { docCode = string(dto.HPPCodeDepresiasi) docLabel = "Depresiasi" } - docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam) - docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice) - hppItems = append(hppItems, dto.ToHPPItem( - 3, - "purchase", - docCode, - docLabel, - dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam), - dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice), - )) + hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost)) + hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational)) + hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost)) - // OVERHEAD item - overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational) - overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization) - hppItems = append(hppItems, dto.ToHPPItem( - 4, - "overhead", - string(dto.HPPCodeOverhead), - "Pengeluaran Overhead", - dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational), - dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization), - )) - - // EKSPEDISI item - ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi) - ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization) - hppItems = append(hppItems, dto.ToHPPItem( - 5, - "overhead", - string(dto.HPPCodeEkspedisi), - "Beban Ekspedisi", - dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi), - dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization), - )) - - // HPP Summary - totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi - totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization + totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost + totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp) hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp) var eggBudgeting, eggRealization *dto.FinancialMetrics - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 { - eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg - eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg - eggBudgeting = &dto.FinancialMetrics{ - RpPerBird: 0, - RpPerKg: eggBudgetRpPerKg, - Amount: totalBudgetHpp, + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) { + if *metrics == nil { + *metrics = &dto.FinancialMetrics{ + RpPerBird: 0, + RpPerKg: rpPerKg, + Amount: amount, + } + } else { + (*metrics).Amount += amount + if totalEggWeightKg > 0 { + (*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg + } + } } - eggRealization = &dto.FinancialMetrics{ - RpPerBird: 0, - RpPerKg: eggRealizationRpPerKg, - Amount: totalRealizationHpp, + + for _, projectFlockKandang := range projectFlockKandangs { + hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil) + if err == nil { + accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg) + accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg) + } } } @@ -543,12 +378,48 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl eggRealization, ) - hppSection := dto.ToHPPSection(hppItems, hppSummary) + return dto.ToHPPSection(hppItems, hppSummary) +} + +func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { + + totalPopulationIn := production.TotalPopulationIn + totalWeightProduced := production.TotalWeightProduced + totalEggWeightKg := production.TotalEggWeightKg + totalSalesAmount := production.TotalSalesAmount + totalWeightSold := production.TotalWeightSold + + weightForSales := totalWeightSold + weightForCalculation := totalWeightProduced + if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + weightForSales = totalWeightSold + weightForCalculation = totalEggWeightKg + } + + calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if totalPopulationIn > 0 { + rpPerBird = amount / totalPopulationIn + } + if weightForSales > 0 { + rpPerKg = amount / weightForSales + } + return + } + + actualPopulation := production.TotalPopulationIn - production.TotalDepletion + + calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if weightForCalculation > 0 { + rpPerKg = amount / weightForCalculation + } + return + } - // Build Profit Loss Items using constants plItems := []dto.ProfitLossItem{} - // SALES item salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { @@ -563,10 +434,13 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl totalSalesAmount, )) - // SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK - totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice - sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird - sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg + totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost + _, sapronakRpPerKg := calculateMetrics(totalSapronakAmount) + sapronakRpPerBird := 0.0 + for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { + rpPerBird, _ := calculateMetrics(amount) + sapronakRpPerBird += rpPerBird + } sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), @@ -577,62 +451,54 @@ func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFl totalSapronakAmount, )) - // OVERHEAD item - overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization) + overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", "overhead", overheadRpPerBird, overheadRpPerKg, - totalOperationalRealization, + costs.RealizationOperational, )) - // EKSPEDISI item + ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", "overhead", - ekspedisiRealizationRpPerBird, - ekspedisiRealizationRpPerKg, - totalEkspedisiRealization, + ekspedisiRpPerBird, + ekspedisiRpPerKg, + costs.ExpeditionCost, )) - // Profit Loss Summary - // Gross Profit = Sales - (DOC + PAKAN + OVK) only - // Gross Profit should NOT include overhead and ekspedisi - costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice + costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost costOfGoodsSoldRpPerBird := sapronakRpPerBird + costOfGoodsSoldRpPerKg := sapronakRpPerKg grossProfit := totalSalesAmount - costOfGoodsSold grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird + grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg - // Operating Expenses (Overhead + Ekspedisi) - totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization - totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird + totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost + totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird + totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg - // Net Profit = Gross Profit - Operating Expenses netProfit := grossProfit - totalOperatingExpenses netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird + netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg plSummary := dto.ToProfitLossSummary( - dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit), - dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses), - dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit), + dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit), + dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses), + dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit), ) - profitLossSection := dto.ToProfitLossSection(plItems, plSummary) - - // Build complete response - data := dto.ToClosingKeuanganData(hppSection, profitLossSection) - - return &data, nil + return dto.ToProfitLossSection(plItems, plSummary) } -// containsItem checks if a string exists in a slice -func containsItem(slice []string, item string) bool { - for _, s := range slice { - if strings.EqualFold(s, item) { +func containsFlag(flags []entity.Flag, name string) bool { + for _, flag := range flags { + if flag.Name == name { return true } } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 1ec0bddf..bcd788cd 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -14,7 +14,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) - GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) + GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) @@ -54,12 +54,14 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo return deliveryProducts, nil } -func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { +func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct db := r.DB().WithContext(ctx). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Where("marketing_delivery_products.delivery_date IS NOT NULL"). @@ -69,6 +71,25 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID) } + if category == string(utils.ProjectFlockCategoryLaying) { + db = db.Where("flags.name IN (?)", []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + }) + } else { + db = db.Where("flags.name IN (?)", []string{ + string(utils.FlagDOC), + string(utils.FlagPullet), + string(utils.FlagLayer), + string(utils.FlagAyamAfkir), + string(utils.FlagAyamCulling), + string(utils.FlagAyamMati), + }) + } + db = db. Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). diff --git a/internal/modules/production/project_flocks/repositories/project_budget.repository.go b/internal/modules/production/project_flocks/repositories/project_budget.repository.go index 720bfc40..06869795 100644 --- a/internal/modules/production/project_flocks/repositories/project_budget.repository.go +++ b/internal/modules/production/project_flocks/repositories/project_budget.repository.go @@ -31,6 +31,7 @@ func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, p Where("project_flock_id = ?", projectFlockID). Preload("Nonstock"). Preload("Nonstock.Uom"). + Preload("Nonstock.Flags"). Find(&budgets).Error return budgets, err } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 6cb65c6c..27c399f4 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -47,8 +47,10 @@ type RecordingRepository interface { GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) + GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) + GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (totalDepletion float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) @@ -473,6 +475,17 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context. return result, err } +func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_depletions"). + Select("COALESCE(SUM(recording_depletions.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). + Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangID). + Scan(&result).Error + return result, err +} + func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { // Body-weight tracking is removed; keep stub for report compatibility. return 0, nil @@ -609,3 +622,23 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF return result.TotalWeight, err } + +func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + if projectFlockKandangID == 0 { + return 0, nil + } + + var result struct { + TotalWeight float64 + } + + err := r.DB().WithContext(ctx). + Table("project_flock_kandang_uniformity"). + Select("COALESCE((mean_up / 1.10) * chick_qty_of_weight / 1000, 0) as total_weight"). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Order("id DESC"). + Limit(1). + Scan(&result).Error + + return result.TotalWeight, err +}