package repository import ( "context" "fmt" "time" "github.com/google/uuid" "gorm.io/gorm" ) // DueLot is a lot whose expiry has passed and that still holds something. type DueLot struct { ID uuid.UUID CustomerID uuid.UUID Currency string Remaining int64 ExpiresAt time.Time // The description of the row that created the lot, for the EXPIRE row's. SourceDescription string } // WalletExpiryRepository finds what the expiry job has to do (docs/prd-point-coin.md // F12). Balances only change through WalletProcessor. type WalletExpiryRepository interface { // ListDueLots returns up to limit lots due at asOf, the longest overdue first. It // takes no lock: locking a lot before its wallet would deadlock against payments, // which lock the wallet first. WalletProcessor.ExpireLot locks and reads again. ListDueLots(ctx context.Context, asOf time.Time, limit int) ([]DueLot, error) } type walletExpiryRepository struct { db *gorm.DB } func NewWalletExpiryRepository(db *gorm.DB) WalletExpiryRepository { return &walletExpiryRepository{db: db} } func (r *walletExpiryRepository) ListDueLots(ctx context.Context, asOf time.Time, limit int) ([]DueLot, error) { var lots []DueLot err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(` SELECT l.id, l.customer_id, l.currency, l.remaining_amount AS remaining, l.expires_at, t.description AS source_description FROM wallet_lots l JOIN wallet_transactions t ON t.id = l.source_transaction_id WHERE l.remaining_amount > 0 AND l.expires_at <= ? ORDER BY l.expires_at, l.id LIMIT ?`, asOf, limit).Scan(&lots).Error if err != nil { return nil, fmt.Errorf("failed to list due wallet lots: %w", err) } return lots, nil }