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)) }