package repository import ( "context" "errors" "fmt" "time" "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) type ExpenseRepository interface { repository.BaseRepository[entity.Expense] IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (int, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) DeleteOne(ctx context.Context, id uint) error GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) } type ExpenseRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.Expense] } func NewExpenseRepository(db *gorm.DB) ExpenseRepository { return &ExpenseRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), } } func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Expense](ctx, r.DB(), id) } func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) { var sequence int err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error if err != nil { return 0, err } return sequence, nil } func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) { var expense entity.Expense err := r.DB().WithContext(ctx). Where("id = ?", id). Preload("Supplier"). First(&expense).Error if err != nil { return nil, err } return &expense, nil } func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if pfkID == 0 && kandangID == 0 { return db } q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id") if pfkID > 0 && kandangID > 0 { return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID) } if pfkID > 0 { return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID) } return q.Where("expense_nonstocks.kandang_id = ?", kandangID) } } func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) { if pfkID == 0 && kandangID == 0 { return 0, nil } var ids []uint64 if err := r.DB().WithContext(ctx). Table("expenses"). Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)). Group("expenses.id").Where("expenses.deleted_at IS NULL"). Pluck("expenses.id", &ids).Error; err != nil { return 0, err } if len(ids) == 0 { return 0, nil } var unfinished int64 for _, id := range ids { var latest entity.Approval err := r.DB().WithContext(ctx). Table("approvals"). Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id). Order("action_at DESC"). Limit(1). First(&latest).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return 0, err } if isFinished != nil { if !isFinished(&latest) { unfinished++ } } } return unfinished, nil } func (r *ExpenseRepositoryImpl) DeleteOne(ctx context.Context, id uint) error { // Cast to uint64 to match entity.Id type id64 := uint64(id) deletedAt := time.Now() // Use raw SQL with interpolated integer to avoid type issues // Interpolate id directly as integer literal (safe because it's uint64) result := r.DB().WithContext(ctx). Exec(`UPDATE "expenses" SET "deleted_at" = $1 WHERE "id" = `+fmt.Sprintf("%d", id64)+` AND "deleted_at" IS NULL`, deletedAt) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil } func (r *ExpenseRepositoryImpl) GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) { const unassignedSQL = "'" + exportprogress.UnassignedKandangName + "'" query := r.DB().WithContext(ctx). Table("expenses AS e"). Select(` 'Expenses' AS module, COALESCE(pf.flock_name, loc.name, fallback_loc.name, 'Unknown Farm') AS farm_name, COALESCE(k.name, `+unassignedSQL+`, 'Unknown Kandang') AS kandang_name, CAST(DATE(e.transaction_date) AS TEXT) AS activity_date, COUNT(*) AS count `). Joins("LEFT JOIN (SELECT DISTINCT expense_id, project_flock_kandang_id, kandang_id FROM expense_nonstocks) en ON en.expense_id = e.id"). Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = en.project_flock_kandang_id"). Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). Joins("LEFT JOIN kandangs k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id)"). Joins("LEFT JOIN locations loc ON loc.id = k.location_id"). Joins("LEFT JOIN locations fallback_loc ON fallback_loc.id = e.location_id"). Where("e.deleted_at IS NULL"). Where("DATE(e.transaction_date) >= DATE(?)", startDate). Where("DATE(e.transaction_date) <= DATE(?)", endDate) if restrict { if len(allowedLocationIDs) == 0 { return []exportprogress.Row{}, nil } query = query.Where("e.location_id IN ?", allowedLocationIDs) } type progressRowResult struct { Module string FarmName string KandangName string ActivityDate string Count int } scanned := make([]progressRowResult, 0) err := query. Group("DATE(e.transaction_date), COALESCE(pf.flock_name, loc.name, fallback_loc.name, 'Unknown Farm'), COALESCE(k.name, " + unassignedSQL + ", 'Unknown Kandang')"). Order("activity_date ASC, farm_name ASC, kandang_name ASC"). Scan(&scanned).Error if err != nil { return nil, err } rows := make([]exportprogress.Row, 0, len(scanned)) for _, item := range scanned { activityDate, err := exportprogress.ParseActivityDate(item.ActivityDate) if err != nil { return nil, err } rows = append(rows, exportprogress.Row{ Module: item.Module, FarmName: item.FarmName, KandangName: item.KandangName, ActivityDate: activityDate, Count: item.Count, }) } return rows, nil }