package repositories import ( "context" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" "gorm.io/gorm/clause" ) type FarmDepreciationCandidateRow struct { ProjectFlockID uint FarmName string } type FarmDepreciationLatestTransferRow struct { ProjectFlockID uint FarmName string ProjectFlockKandangID uint KandangID uint KandangName string HouseType *string SourceProjectFlockID uint TransferDate time.Time TransferQty float64 TransferID uint } type FarmDepreciationManualInputRow struct { Id uint ProjectFlockID uint FarmName string TotalCost float64 Note *string } type houseDepreciationPercentRow struct { HouseType string Day int DepreciationPercent float64 } type ExpenseDepreciationRepository interface { GetCandidateFarms(ctx context.Context, period time.Time, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationCandidateRow, error) GetSnapshotsByPeriodAndFarmIDs(ctx context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error GetLatestTransferInputsByFarms(ctx context.Context, period time.Time, farmIDs []uint) ([]FarmDepreciationLatestTransferRow, error) GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) GetLatestManualInputsByFarms(ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationManualInputRow, error) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error DB() *gorm.DB } type expenseDepreciationRepository struct { db *gorm.DB } func NewExpenseDepreciationRepository(db *gorm.DB) ExpenseDepreciationRepository { return &expenseDepreciationRepository{db: db} } func (r *expenseDepreciationRepository) DB() *gorm.DB { return r.db } func (r *expenseDepreciationRepository) GetCandidateFarms( ctx context.Context, period time.Time, areaIDs, locationIDs, projectFlockIDs []int64, ) ([]FarmDepreciationCandidateRow, error) { rows := make([]FarmDepreciationCandidateRow, 0) query := r.db.WithContext(ctx). Table("project_flocks AS pf"). Select("DISTINCT pf.id AS project_flock_id, pf.flock_name AS farm_name"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id"). Where("pf.deleted_at IS NULL"). Where("pf.category = ?", utils.ProjectFlockCategoryLaying). Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", period) if len(areaIDs) > 0 { query = query.Where("pf.area_id IN ?", areaIDs) } if len(locationIDs) > 0 { query = query.Where("pf.location_id IN ?", locationIDs) } if len(projectFlockIDs) > 0 { query = query.Where("pf.id IN ?", projectFlockIDs) } if err := query.Order("pf.id ASC").Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *expenseDepreciationRepository) GetSnapshotsByPeriodAndFarmIDs( ctx context.Context, period time.Time, farmIDs []uint, ) ([]entity.FarmDepreciationSnapshot, error) { if len(farmIDs) == 0 { return []entity.FarmDepreciationSnapshot{}, nil } rows := make([]entity.FarmDepreciationSnapshot, 0) if err := r.db.WithContext(ctx). Where("project_flock_id IN ?", farmIDs). Where("period_date = DATE(?)", period). Find(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *expenseDepreciationRepository) UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error { if len(rows) == 0 { return nil } return r.db.WithContext(ctx). Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "project_flock_id"}, {Name: "period_date"}, }, DoUpdates: clause.AssignmentColumns([]string{ "depreciation_percent_effective", "depreciation_value", "pullet_cost_day_n_total", "components", "updated_at", }), }). Create(&rows).Error } func (r *expenseDepreciationRepository) DeleteSnapshotsFromDate( ctx context.Context, fromDate time.Time, farmIDs []uint, ) error { if fromDate.IsZero() { return nil } query := r.db.WithContext(ctx). Table("farm_depreciation_snapshots"). Where("period_date >= DATE(?)", fromDate) if len(farmIDs) > 0 { query = query.Where("project_flock_id IN ?", farmIDs) } return query.Delete(nil).Error } func (r *expenseDepreciationRepository) GetLatestTransferInputsByFarms( ctx context.Context, period time.Time, farmIDs []uint, ) ([]FarmDepreciationLatestTransferRow, error) { if len(farmIDs) == 0 { return []FarmDepreciationLatestTransferRow{}, nil } rows := make([]FarmDepreciationLatestTransferRow, 0) 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, lt.to_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 DISTINCT ON (ltt.target_project_flock_kandang_id) pf.id AS project_flock_id, pf.flock_name AS farm_name, pfk.id AS project_flock_kandang_id, k.id AS kandang_id, k.name AS kandang_name, k.house_type::text AS house_type, 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 JOIN project_flock_kandangs pfk ON pfk.id = ltt.target_project_flock_kandang_id JOIN project_flocks pf ON pf.id = pfk.project_flock_id JOIN kandangs k ON k.id = pfk.kandang_id WHERE ltt.deleted_at IS NULL AND pf.id IN @farm_ids AND at.effective_date <= DATE(@period_date) ORDER BY ltt.target_project_flock_kandang_id, at.effective_date DESC, at.id DESC ` if err := r.db.WithContext(ctx).Raw(query, map[string]any{ "approval_type": utils.ApprovalWorkflowTransferToLaying.String(), "farm_ids": farmIDs, "period_date": period, }).Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *expenseDepreciationRepository) 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 } rows := make([]houseDepreciationPercentRow, 0) if 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; err != nil { return nil, err } for _, row := range rows { if _, exists := result[row.HouseType]; !exists { result[row.HouseType] = make(map[int]float64) } result[row.HouseType][row.Day] = row.DepreciationPercent } return result, nil } func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms( ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64, ) ([]FarmDepreciationManualInputRow, error) { rows := make([]FarmDepreciationManualInputRow, 0) query := r.db.WithContext(ctx). Table("farm_depreciation_manual_inputs AS fdmi"). Select(` fdmi.id AS id, fdmi.project_flock_id AS project_flock_id, pf.flock_name AS farm_name, fdmi.total_cost AS total_cost, fdmi.note AS note `). Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id"). Where("pf.deleted_at IS NULL"). Where("pf.category = ?", utils.ProjectFlockCategoryLaying) if len(areaIDs) > 0 { query = query.Where("pf.area_id IN ?", areaIDs) } if len(locationIDs) > 0 { query = query.Where("pf.location_id IN ?", locationIDs) } if len(projectFlockIDs) > 0 { query = query.Where("pf.id IN ?", projectFlockIDs) } if err := query. Order("pf.id ASC"). Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error { if row == nil { return nil } now := time.Now().UTC() err := r.db.WithContext(ctx). Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "project_flock_id"}, }, DoUpdates: clause.Assignments(map[string]any{ "total_cost": row.TotalCost, "note": row.Note, "updated_at": now, }), }). Create(row).Error if err != nil { return err } return r.db.WithContext(ctx). Table("farm_depreciation_manual_inputs"). Select("id, project_flock_id, total_cost, note"). Where("project_flock_id = ?", row.ProjectFlockId). Take(row).Error }