package repository import ( "context" "errors" "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) type HppV2ProjectFlockKandangContext struct { ProjectFlockKandangID uint ProjectFlockID uint ProjectFlockCategory string KandangID uint KandangName string LocationID uint HouseType string } type HppV2UsageCostRow struct { StockableType string StockableID uint SourceProductID uint SourceProductName string Qty float64 UnitPrice float64 TotalCost float64 FirstUsedAt time.Time LastUsedAt time.Time } type HppV2AdjustmentCostRow struct { AdjustmentID uint ProjectFlockKandangID *uint ProductWarehouseID uint ProductID uint ProductName string WarehouseID uint WarehouseType string Qty float64 Price float64 GrandTotal float64 CreatedAt time.Time } type HppV2ExpenseCostRow struct { ExpenseRealizationID uint ExpenseNonstockID uint ExpenseID uint NonstockID uint NonstockName string Qty float64 Price float64 TotalCost float64 RealizationDate time.Time } type HppV2ChickinCostRow struct { ProjectChickinID uint ProjectFlockKandangID uint ChickInDate time.Time StockableType string StockableID uint SourceProductID uint SourceProductName string Qty float64 UnitPrice float64 TotalCost float64 } type HppV2LatestTransferInputRow struct { ProjectFlockKandangID uint SourceProjectFlockID uint TransferDate time.Time TransferQty float64 TransferID uint } type HppV2ManualDepreciationInputRow struct { ID uint ProjectFlockID uint TotalCost float64 CutoverDate time.Time Note *string } type HppV2FarmDepreciationSnapshotRow struct { ID uint ProjectFlockID uint PeriodDate time.Time DepreciationPercentEffective float64 DepreciationValue float64 PulletCostDayNTotal float64 } type HppV2CostRepository interface { GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error) GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error) GetRecordingStockRoutingAdjustmentCostByProjectFlockID(ctx context.Context, projectFlockID uint, periodDate time.Time) (float64, error) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error) ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error) ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error) ListExpenseRealizationRowsByProjectFlockID(ctx context.Context, projectFlockID uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error) ListChickinCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, excludeTransferToLaying bool) ([]HppV2ChickinCostRow, error) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) GetEggProduksiBreakdownByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (recordingQty, recordingWeight, adjustmentQty, adjustmentWeight float64, err error) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) } type HppV2RepositoryImpl struct { db *gorm.DB } func NewHppV2CostRepository(db *gorm.DB) HppV2CostRepository { return &HppV2RepositoryImpl{db: db} } func (r *HppV2RepositoryImpl) GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error) { var row HppV2ProjectFlockKandangContext err := r.db.WithContext(ctx). Table("project_flock_kandangs AS pfk"). Select(` pfk.id AS project_flock_kandang_id, pf.id AS project_flock_id, pf.category AS project_flock_category, k.id AS kandang_id, k.name AS kandang_name, k.location_id AS location_id, k.house_type::text AS house_type `). Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Where("pfk.id = ?", projectFlockKandangId). Scan(&row).Error if err != nil { return nil, err } if row.ProjectFlockKandangID == 0 { return nil, gorm.ErrRecordNotFound } return &row, nil } func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) { var ids []uint err := r.db.WithContext(ctx). Table("project_flock_kandangs"). Select("id"). Where("project_flock_id = ?", projectFlockId). Scan(&ids).Error if err != nil { return nil, err } return ids, nil } func (r *HppV2RepositoryImpl) GetLatestTransferInputByProjectFlockKandangID( ctx context.Context, projectFlockKandangId uint, period time.Time, ) (*HppV2LatestTransferInputRow, error) { var row HppV2LatestTransferInputRow query := ` WITH latest_transfer_approval AS ( SELECT a.approvable_id, a.action FROM approvals a JOIN ( SELECT approvable_id, MAX(action_at) AS latest_action_at FROM approvals WHERE approvable_type = @approval_type GROUP BY approvable_id ) la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at WHERE a.approvable_type = @approval_type ), approved_transfers AS ( SELECT lt.id, lt.from_project_flock_id, COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date FROM laying_transfers lt JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id WHERE lt.deleted_at IS NULL AND lt.executed_at IS NOT NULL AND lta.action = 'APPROVED' ) SELECT ltt.target_project_flock_kandang_id AS project_flock_kandang_id, at.from_project_flock_id AS source_project_flock_id, at.effective_date AS transfer_date, ltt.total_qty AS transfer_qty, at.id AS transfer_id FROM laying_transfer_targets ltt JOIN approved_transfers at ON at.id = ltt.laying_transfer_id WHERE ltt.deleted_at IS NULL AND ltt.target_project_flock_kandang_id = @project_flock_kandang_id AND at.effective_date <= DATE(@period_date) ORDER BY at.effective_date DESC, at.id DESC LIMIT 1 ` err := r.db.WithContext(ctx).Raw(query, map[string]any{ "approval_type": utils.ApprovalWorkflowTransferToLaying.String(), "project_flock_kandang_id": projectFlockKandangId, "period_date": period, }).Scan(&row).Error if err != nil { return nil, err } if row.TransferID == 0 { return nil, nil } return &row, nil } func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID( ctx context.Context, projectFlockID uint, ) (*HppV2ManualDepreciationInputRow, error) { var row HppV2ManualDepreciationInputRow err := r.db.WithContext(ctx). Table("farm_depreciation_manual_inputs"). Select("id, project_flock_id, total_cost, cutover_date, note"). Where("project_flock_id = ?", projectFlockID). Limit(1). Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } return &row, nil } func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlockID( ctx context.Context, projectFlockID uint, periodDate time.Time, ) (float64, error) { if projectFlockID == 0 || periodDate.IsZero() { return 0, nil } flags := []utils.FlagType{ utils.FlagPakan, utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia, } transferExistsCondition := ` EXISTS ( SELECT 1 FROM laying_transfer_targets AS ltt JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id WHERE ltt.deleted_at IS NULL AND lt.deleted_at IS NULL AND lt.executed_at IS NOT NULL AND ltt.target_project_flock_kandang_id = r.project_flock_kandangs_id AND COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) <= DATE(?) AND ( SELECT a.action FROM approvals a WHERE a.approvable_type = ? AND a.approvable_id = lt.id ORDER BY a.id DESC LIMIT 1 ) = ? ) ` var total float64 err := r.db.WithContext(ctx). Table("recording_stocks AS rs"). Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL"). Joins("JOIN project_flock_kandangs AS pfk_rec ON pfk_rec.id = r.project_flock_kandangs_id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). Joins( "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). Where("pfk_rec.project_flock_id = ?", projectFlockID). Where("DATE(r.record_datetime) <= DATE(?)", periodDate). Where( fmt.Sprintf( "((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)", transferExistsCondition, transferExistsCondition, ), periodDate, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved, periodDate, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved, ). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags). Scan(&total).Error if err != nil { return 0, err } return total, nil } func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod( ctx context.Context, projectFlockID uint, periodDate time.Time, ) (*HppV2FarmDepreciationSnapshotRow, error) { var row HppV2FarmDepreciationSnapshotRow err := r.db.WithContext(ctx). Table("farm_depreciation_snapshots"). Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total"). Where("project_flock_id = ?", projectFlockID). Where("period_date = DATE(?)", periodDate). Limit(1). Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } return &row, nil } func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) { type row struct { ChickInDate *time.Time } var selected row err := r.db.WithContext(ctx). Table("project_chickins AS pc"). Select("MIN(pc.chick_in_date) AS chick_in_date"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id"). Where("pc.deleted_at IS NULL"). Where("pfk.project_flock_id = ?", projectFlockID). Scan(&selected).Error if err != nil { return nil, err } if selected.ChickInDate == nil || selected.ChickInDate.IsZero() { return nil, nil } return selected.ChickInDate, nil } func (r *HppV2RepositoryImpl) GetDepreciationPercents( ctx context.Context, houseTypes []string, maxDay int, ) (map[string]map[int]float64, error) { result := make(map[string]map[int]float64) if len(houseTypes) == 0 || maxDay <= 0 { return result, nil } type row struct { HouseType string Day int DepreciationPercent float64 } rows := make([]row, 0) err := r.db.WithContext(ctx). Table("house_depreciation_standards"). Select("house_type::text AS house_type, day, depreciation_percent"). Where("house_type::text IN ?", houseTypes). Where("day <= ?", maxDay). Order("house_type ASC, day ASC"). Scan(&rows).Error if err != nil { return nil, err } for _, item := range rows { if _, exists := result[item.HouseType]; !exists { result[item.HouseType] = make(map[int]float64) } result[item.HouseType][item.Day] = item.DepreciationPercent } return result, nil } func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags( ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, ) ([]HppV2UsageCostRow, error) { if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { return []HppV2UsageCostRow{}, nil } if date == nil { now := time.Now() date = &now } stockablePurchase := fifo.StockableKeyPurchaseItems.String() stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() usableRecordingStock := fifo.UsableKeyRecordingStock.String() rows := make([]HppV2UsageCostRow, 0) err := r.db.WithContext(ctx). Table("recordings AS r"). Select(` sa.stockable_type AS stockable_type, sa.stockable_id AS stockable_id, COALESCE(pi.product_id, ast_pw.product_id, 0) AS source_product_id, COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name, COALESCE(SUM(sa.qty), 0) AS qty, COALESCE(MAX(CASE WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) ELSE 0 END), 0) AS unit_price, COALESCE(SUM(sa.qty * CASE WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) ELSE 0 END), 0) AS total_cost, MIN(r.record_datetime) AS first_used_at, MAX(r.record_datetime) AS last_used_at `, stockablePurchase, stockableAdjustment, stockablePurchase, stockableAdjustment, ). 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 stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?", usableRecordingStock, stockablePurchase, stockableAdjustment, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN products AS pi_prod ON pi_prod.id = pi.product_id"). Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id"). Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id"). Where("rs.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("r.record_datetime <= ?", *date). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames). Group(` sa.stockable_type, sa.stockable_id, COALESCE(pi.product_id, ast_pw.product_id, 0), COALESCE(pi_prod.name, ast_prod.name, '') `). Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Scan(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags( ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, ) ([]HppV2AdjustmentCostRow, error) { if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { return []HppV2AdjustmentCostRow{}, nil } if date == nil { now := time.Now() date = &now } rows := make([]HppV2AdjustmentCostRow, 0) err := r.db.WithContext(ctx). Table("adjustment_stocks AS ast"). Select(` ast.id AS adjustment_id, pw.project_flock_kandang_id AS project_flock_kandang_id, ast.product_warehouse_id AS product_warehouse_id, pw.product_id AS product_id, p.name AS product_name, w.id AS warehouse_id, w.type AS warehouse_type, COALESCE(ast.total_qty, 0) AS qty, COALESCE(ast.price, 0) AS price, COALESCE(ast.grand_total, 0) AS grand_total, ast.created_at AS created_at `). Joins("JOIN product_warehouses AS pw ON pw.id = ast.product_warehouse_id"). Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id"). Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). // Where("ast.created_at <= ?", *date). Where("COALESCE(ast.total_qty, 0) > 0"). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames). Order("ast.created_at ASC, ast.id ASC"). Scan(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags( ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time, excludeTransferToLaying bool, ) ([]HppV2ChickinCostRow, error) { if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { return []HppV2ChickinCostRow{}, nil } if date == nil { now := time.Now() date = &now } stockablePurchase := fifo.StockableKeyPurchaseItems.String() stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() stockableTransferIn := fifo.StockableKeyStockTransferIn.String() stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String() usableProjectChickin := fifo.UsableKeyProjectChickin.String() usableStockTransferOut := fifo.UsableKeyStockTransferOut.String() unitPriceExpr := fmt.Sprintf(` CASE WHEN sa.stockable_type = '%s' THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = '%s' THEN COALESCE(ast.price, 0) WHEN sa.stockable_type = '%s' THEN COALESCE(spi.price, sast.price, 0) WHEN sa.stockable_type = '%s' THEN COALESCE(tpi.price, tast.price, 0) ELSE 0 END `, stockablePurchase, stockableAdjustment, stockableTransferIn, stockableTransferToLaying) rows := make([]HppV2ChickinCostRow, 0) query := r.db.WithContext(ctx). Table("project_chickins AS pc"). Select(` pc.id AS project_chickin_id, pc.project_flock_kandang_id AS project_flock_kandang_id, pc.chick_in_date AS chick_in_date, sa.stockable_type AS stockable_type, sa.stockable_id AS stockable_id, COALESCE( pi.product_id, ast_pw.product_id, tpi.product_id, tast_pw.product_id, spi.product_id, sast_pw.product_id, 0 ) AS source_product_id, COALESCE( pi_prod.name, ast_prod.name, tpi_prod.name, tast_prod.name, spi_prod.name, sast_prod.name, '' ) AS source_product_name, COALESCE(SUM(sa.qty), 0) AS qty, `+unitPriceExpr+` AS unit_price, COALESCE(SUM(sa.qty * (`+unitPriceExpr+`)), 0) AS total_cost `). Joins( "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", usableProjectChickin, entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin, ). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN products AS pi_prod ON pi_prod.id = pi.product_id"). Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Joins("LEFT JOIN product_warehouses AS ast_pw ON ast_pw.id = ast.product_warehouse_id"). Joins("LEFT JOIN products AS ast_prod ON ast_prod.id = ast_pw.product_id"). Joins( "LEFT JOIN stock_allocations AS tsa_transfer ON tsa_transfer.usable_type = ? AND tsa_transfer.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_transfer.status = ? AND tsa_transfer.allocation_purpose = ?", stockableTransferToLaying, stockableTransferToLaying, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN products AS tpi_prod ON tpi_prod.id = tpi.product_id"). Joins("LEFT JOIN adjustment_stocks AS tast ON tast.id = tsa_transfer.stockable_id AND tsa_transfer.stockable_type = ?", stockableAdjustment). Joins("LEFT JOIN product_warehouses AS tast_pw ON tast_pw.id = tast.product_warehouse_id"). Joins("LEFT JOIN products AS tast_prod ON tast_prod.id = tast_pw.product_id"). Joins( "LEFT JOIN stock_allocations AS tsa_stock ON tsa_stock.usable_type = ? AND tsa_stock.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa_stock.status = ? AND tsa_stock.allocation_purpose = ?", usableStockTransferOut, stockableTransferIn, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Joins("LEFT JOIN purchase_items AS spi ON spi.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN products AS spi_prod ON spi_prod.id = spi.product_id"). Joins("LEFT JOIN adjustment_stocks AS sast ON sast.id = tsa_stock.stockable_id AND tsa_stock.stockable_type = ?", stockableAdjustment). Joins("LEFT JOIN product_warehouses AS sast_pw ON sast_pw.id = sast.product_warehouse_id"). Joins("LEFT JOIN products AS sast_prod ON sast_prod.id = sast_pw.product_id"). Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("pc.chick_in_date <= ?", *date). Where(` EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_type = ? AND f.flagable_id = COALESCE( pi.product_id, ast_pw.product_id, tpi.product_id, tast_pw.product_id, spi.product_id, sast_pw.product_id, 0 ) AND f.name IN ? ) `, entity.FlagableTypeProduct, flagNames) if excludeTransferToLaying { query = query.Where("sa.stockable_type <> ?", stockableTransferToLaying) } err := query. Group(fmt.Sprintf(` pc.id, pc.project_flock_kandang_id, pc.chick_in_date, sa.stockable_type, sa.stockable_id, COALESCE( pi.product_id, ast_pw.product_id, tpi.product_id, tast_pw.product_id, spi.product_id, sast_pw.product_id, 0 ), COALESCE( pi_prod.name, ast_prod.name, tpi_prod.name, tast_prod.name, spi_prod.name, sast_prod.name, '' ), %s `, unitPriceExpr)). Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Scan(&rows).Error if err != nil { return nil, err } return rows, nil } func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockKandangIDs( ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool, ) ([]HppV2ExpenseCostRow, error) { if len(projectFlockKandangIDs) == 0 { return []HppV2ExpenseCostRow{}, nil } if date == nil { now := time.Now() date = &now } rows := make([]HppV2ExpenseCostRow, 0) query := r.db.WithContext(ctx). Table("expense_realizations AS er"). Select(` er.id AS expense_realization_id, en.id AS expense_nonstock_id, e.id AS expense_id, COALESCE(n.id, 0) AS nonstock_id, COALESCE(n.name, '') AS nonstock_name, COALESCE(er.qty, 0) AS qty, COALESCE(er.price, 0) AS price, COALESCE(er.qty, 0) * COALESCE(er.price, 0) AS total_cost, COALESCE(e.realization_date, DATE(er.created_at)) AS realization_date `). Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id"). Joins("JOIN expenses AS e ON e.id = en.expense_id"). Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id"). Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi). Where("e.deleted_at IS NULL"). Where("e.category = ?", utils.ExpenseCategoryBOP). Where("en.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("COALESCE(e.realization_date, DATE(er.created_at)) <= ?", *date) if ekspedisi { query = query.Where("f.id IS NOT NULL") } else { query = query.Where("f.id IS NULL") } if err := query. Order("COALESCE(e.realization_date, DATE(er.created_at)) ASC, er.id ASC"). Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *HppV2RepositoryImpl) ListExpenseRealizationRowsByProjectFlockID( ctx context.Context, projectFlockID uint, date *time.Time, ekspedisi bool, ) ([]HppV2ExpenseCostRow, error) { if projectFlockID == 0 { return []HppV2ExpenseCostRow{}, nil } if date == nil { now := time.Now() date = &now } rows := make([]HppV2ExpenseCostRow, 0) query := r.db.WithContext(ctx). Table("expense_realizations AS er"). Select(` er.id AS expense_realization_id, en.id AS expense_nonstock_id, e.id AS expense_id, COALESCE(n.id, 0) AS nonstock_id, COALESCE(n.name, '') AS nonstock_name, COALESCE(er.qty, 0) AS qty, COALESCE(er.price, 0) AS price, COALESCE(er.qty, 0) * COALESCE(er.price, 0) AS total_cost, COALESCE(e.realization_date, DATE(er.created_at)) AS realization_date `). Joins("JOIN expense_nonstocks AS en ON en.id = er.expense_nonstock_id"). Joins("JOIN expenses AS e ON e.id = en.expense_id"). Joins("LEFT JOIN nonstocks AS n ON n.id = en.nonstock_id"). Joins("LEFT JOIN flags AS f ON f.flagable_id = n.id AND f.flagable_type = ? AND f.name = ?", entity.FlagableTypeNonstock, utils.FlagEkspedisi). Where("e.deleted_at IS NULL"). Where("e.category = ?", utils.ExpenseCategoryBOP). Where("en.project_flock_kandang_id IS NULL"). Where("e.project_flock_id IS NOT NULL"). Where("e.project_flock_id::jsonb @> ?::jsonb", fmt.Sprintf("[%d]", projectFlockID)). Where("COALESCE(e.realization_date, DATE(er.created_at)) <= ?", *date) if ekspedisi { query = query.Where("f.id IS NOT NULL") } else { query = query.Where("f.id IS NULL") } if err := query. Order("COALESCE(e.realization_date, DATE(er.created_at)) ASC, er.id ASC"). Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *HppV2RepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) { if date == nil { now := time.Now() date = &now } stockablePurchase := fifo.StockableKeyPurchaseItems.String() stockableAdjustment := fifo.StockableKeyAdjustmentIn.String() usableRecordingStock := fifo.UsableKeyRecordingStock.String() var total float64 err := r.db.WithContext(ctx). Table("recordings AS r"). Select(` COALESCE(SUM(sa.qty * CASE WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) ELSE 0 END), 0)`, stockablePurchase, stockableAdjustment, ). 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 flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins( "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?", usableRecordingStock, stockablePurchase, stockableAdjustment, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment). Where("rs.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Where("r.record_datetime <= ?", *date). Where("f.name = ?", utils.FlagPakan). Scan(&total).Error if err != nil { return 0, err } return total, nil } func (r *HppV2RepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { var total float64 err := r.db.WithContext(ctx). Table("project_chickins AS pc"). Select("COALESCE(SUM(pc.usage_qty), 0)"). Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Scan(&total).Error if err != nil { return 0, err } return total, nil } func (r *HppV2RepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) { rQty, rWeight, aQty, aWeight, err := r.GetEggProduksiBreakdownByProjectFlockKandangIds(ctx, projectFlockKandangIDs, date) if err != nil { return 0, 0, err } return rQty + aQty, rWeight + aWeight, nil } func (r *HppV2RepositoryImpl) GetEggProduksiBreakdownByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (recordingQty, recordingWeight, adjustmentQty, adjustmentWeight float64, err error) { if date == nil { now := time.Now() date = &now } var recordingTotals struct { TotalPieces float64 TotalWeightKg float64 } err = r.db.WithContext(ctx). Table("recordings AS r"). Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0) AS total_weight_kg"). Joins("JOIN recording_eggs AS re ON re.recording_id = r.id"). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.record_datetime <= ?", *date). Scan(&recordingTotals).Error if err != nil { return 0, 0, 0, 0, err } var adjustmentTotals struct { TotalQty float64 TotalWeight float64 } err = r.db.WithContext(ctx). Table("adjustment_stocks AS ast"). Select("COALESCE(SUM(ast.total_qty), 0) AS total_qty, COALESCE(SUM(ast.price), 0) AS total_weight"). Joins("JOIN product_warehouses AS pw ON pw.id = ast.product_warehouse_id"). Where("pw.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Where("ast.function_code = ?", string(utils.AdjustmentTransactionSubtypeRecordingEggIn)). Scan(&adjustmentTotals).Error if err != nil { return 0, 0, 0, 0, err } return recordingTotals.TotalPieces, recordingTotals.TotalWeightKg, adjustmentTotals.TotalQty, adjustmentTotals.TotalWeight, nil } func (r *HppV2RepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds( ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time, ) (float64, float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, 0, nil } if endDate == nil { now := time.Now() endDate = &now } if startDate == nil { startDate = endDate } eggFlags := []string{ string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak), string(utils.FlagTelurPapacal), string(utils.FlagTelurJumbo), } query := ` WITH selected_pfk AS ( SELECT pfk.id, k.location_id FROM project_flock_kandangs pfk JOIN kandangs k ON k.id = pfk.kandang_id WHERE pfk.id IN ? ), selected_locations AS ( SELECT DISTINCT location_id FROM selected_pfk ), sales_kandang AS ( SELECT DISTINCT mdp.id AS mdp_id, COALESCE(mdp.usage_qty, 0) AS usage_qty, COALESCE(mdp.total_weight, 0) AS total_weight 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 warehouses w ON w.id = pw.warehouse_id WHERE mdp.delivery_date IS NOT NULL AND mdp.delivery_date <= ? AND UPPER(COALESCE(w.type, '')) = 'KANDANG' AND pw.project_flock_kandang_id IN (SELECT id FROM selected_pfk) AND EXISTS ( SELECT 1 FROM recording_eggs re JOIN recordings rr ON rr.id = re.recording_id WHERE re.product_warehouse_id = mp.product_warehouse_id AND COALESCE(re.project_flock_kandang_id, rr.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk) AND rr.deleted_at IS NULL AND DATE(rr.record_datetime) <= DATE(mdp.delivery_date) ) AND EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_type = ? AND f.flagable_id = pw.product_id AND f.name IN ? ) ), sales_lokasi AS ( SELECT DISTINCT mdp.id AS mdp_id, COALESCE(mdp.usage_qty, 0) AS usage_qty, COALESCE(mdp.total_weight, 0) AS total_weight, mdp.delivery_date AS delivery_date, pw.id AS lokasi_pw_id, pw.product_id AS product_id, w.location_id AS location_id 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 warehouses w ON w.id = pw.warehouse_id WHERE mdp.delivery_date IS NOT NULL AND mdp.delivery_date <= ? AND UPPER(COALESCE(w.type, '')) = 'LOKASI' AND w.location_id IN (SELECT location_id FROM selected_locations) AND EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_type = ? AND f.flagable_id = pw.product_id AND f.name IN ? ) ), transfer_pairs AS ( SELECT std.source_product_warehouse_id AS source_pw_id, std.dest_product_warehouse_id AS dest_pw_id, MIN(st.transfer_date) AS first_transfer_date FROM stock_transfer_details std JOIN stock_transfers st ON st.id = std.stock_transfer_id WHERE std.source_product_warehouse_id IS NOT NULL AND std.dest_product_warehouse_id IS NOT NULL GROUP BY std.source_product_warehouse_id, std.dest_product_warehouse_id ), adj_pool AS ( SELECT sl.mdp_id, SUM(CASE WHEN spw.project_flock_kandang_id IN (SELECT id FROM selected_pfk) THEN COALESCE(ast.usage_qty, 0) ELSE 0 END) AS sel_usage_qty, SUM(COALESCE(ast.usage_qty, 0)) AS farm_usage_qty, SUM(CASE WHEN spw.project_flock_kandang_id IN (SELECT id FROM selected_pfk) THEN COALESCE(ast.price, 0) ELSE 0 END) AS sel_price_sum, SUM(COALESCE(ast.price, 0)) AS farm_price_sum FROM sales_lokasi sl JOIN transfer_pairs tf ON tf.dest_pw_id = sl.lokasi_pw_id AND DATE(tf.first_transfer_date) <= DATE(sl.delivery_date) JOIN product_warehouses spw ON spw.id = tf.source_pw_id AND spw.product_id = sl.product_id JOIN warehouses sw ON sw.id = spw.warehouse_id JOIN adjustment_stocks ast ON ast.product_warehouse_id = tf.source_pw_id WHERE UPPER(COALESCE(sw.type, '')) = 'KANDANG' AND sw.location_id = sl.location_id AND UPPER(COALESCE(ast.function_code, '')) = UPPER(?) AND UPPER(COALESCE(ast.transaction_type, '')) = UPPER(?) AND DATE(ast.created_at) <= DATE(sl.delivery_date) GROUP BY sl.mdp_id ), sales_lokasi_adj AS ( SELECT sl.* FROM sales_lokasi sl JOIN adj_pool ap ON ap.mdp_id = sl.mdp_id WHERE COALESCE(ap.farm_usage_qty, 0) > 0 OR COALESCE(ap.farm_price_sum, 0) > 0 ), sales_lokasi_rec AS ( SELECT sl.* FROM sales_lokasi sl WHERE NOT EXISTS ( SELECT 1 FROM sales_lokasi_adj sla WHERE sla.mdp_id = sl.mdp_id ) ), rec_pool AS ( SELECT sl.mdp_id, SUM(CASE WHEN COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk) THEN COALESCE(re.qty, 0) ELSE 0 END) AS sel_qty, SUM(COALESCE(re.qty, 0)) AS farm_qty, SUM(CASE WHEN COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) IN (SELECT id FROM selected_pfk) THEN COALESCE(re.weight, 0) ELSE 0 END) AS sel_weight, SUM(COALESCE(re.weight, 0)) AS farm_weight FROM sales_lokasi_rec sl JOIN recordings r ON r.deleted_at IS NULL AND DATE(r.record_datetime) <= DATE(sl.delivery_date) JOIN recording_eggs re ON re.recording_id = r.id AND re.product_warehouse_id = sl.lokasi_pw_id JOIN project_flock_kandangs pfk ON pfk.id = COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) JOIN kandangs k ON k.id = pfk.kandang_id WHERE k.location_id = sl.location_id GROUP BY sl.mdp_id ), kandang_totals AS ( SELECT COALESCE(SUM(sk.usage_qty), 0) AS total_pieces, COALESCE(SUM(sk.total_weight), 0) AS total_weight FROM sales_kandang sk ), lokasi_adj_totals AS ( SELECT COALESCE(SUM( sla.usage_qty * CASE WHEN COALESCE(ap.farm_usage_qty, 0) > 0 THEN (COALESCE(ap.sel_usage_qty, 0) * 1.0) / NULLIF(ap.farm_usage_qty, 0) ELSE 0 END ), 0) AS total_pieces, COALESCE(SUM( sla.total_weight * CASE WHEN COALESCE(ap.farm_price_sum, 0) > 0 THEN (COALESCE(ap.sel_price_sum, 0) * 1.0) / NULLIF(ap.farm_price_sum, 0) ELSE 0 END ), 0) AS total_weight FROM sales_lokasi_adj sla JOIN adj_pool ap ON ap.mdp_id = sla.mdp_id ), lokasi_rec_totals AS ( SELECT COALESCE(SUM( slr.usage_qty * CASE WHEN COALESCE(rp.farm_qty, 0) > 0 THEN (COALESCE(rp.sel_qty, 0) * 1.0) / NULLIF(rp.farm_qty, 0) ELSE 0 END ), 0) AS total_pieces, COALESCE(SUM( slr.total_weight * CASE WHEN COALESCE(rp.farm_weight, 0) > 0 THEN (COALESCE(rp.sel_weight, 0) * 1.0) / NULLIF(rp.farm_weight, 0) ELSE 0 END ), 0) AS total_weight FROM sales_lokasi_rec slr LEFT JOIN rec_pool rp ON rp.mdp_id = slr.mdp_id ) SELECT COALESCE(kt.total_pieces, 0) + COALESCE(lat.total_pieces, 0) + COALESCE(lrt.total_pieces, 0) AS total_pieces, COALESCE(kt.total_weight, 0) + COALESCE(lat.total_weight, 0) + COALESCE(lrt.total_weight, 0) AS total_weight FROM kandang_totals kt CROSS JOIN lokasi_adj_totals lat CROSS JOIN lokasi_rec_totals lrt ` var totals struct { TotalPieces float64 TotalWeight float64 } err := r.db.WithContext(ctx). Raw( query, projectFlockKandangIDs, *endDate, entity.FlagableTypeProduct, eggFlags, *endDate, entity.FlagableTypeProduct, eggFlags, string(utils.AdjustmentTransactionSubtypeRecordingEggIn), string(utils.AdjustmentTransactionTypeRecording), ). Scan(&totals).Error if err != nil { return 0, 0, err } return totals.TotalPieces, totals.TotalWeight, nil } func (r *HppV2RepositoryImpl) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) { var summary struct { ProjectFlockID uint TotalQty float64 } err := r.db.WithContext(ctx). Table("laying_transfer_targets AS ltt"). Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty"). Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id"). Where("lt.deleted_at IS NULL"). Where("ltt.deleted_at IS NULL"). Where("lt.executed_at IS NOT NULL"). Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId). Group("lt.from_project_flock_id"). Scan(&summary).Error if err != nil { return 0, 0, err } return summary.ProjectFlockID, summary.TotalQty, nil }