package payments import ( "context" "crussell/db" ) // Gift-card purchase/transaction limits (owner decisions). // // - Every admin gift-card value operation (CreateGiftCard, TopUpGiftCard, // TransferGiftCard) is capped at £250 per transaction — tighter than the // £10,000 ceiling ValidateAmount enforces on other payment entry points. // - A customer (BuyGiftCard) may buy at most £500 of online gift cards per // UTC day. // - An admin may create/top-up/transfer at most £5,000 of gift-card value // per UTC day. // // till.go uses the same £250 transaction cap (maxAdminGiftCardTransactionPence) // for its gift-card creates/topups — this shared constant is the single source // of the owner decision. const ( // maxAdminGiftCardTransactionPence caps a single admin gift-card // create/top-up/transfer at £250 (25,000 pence). maxAdminGiftCardTransactionPence = 25_000 // maxUserGiftCardDailyPence caps one user's online gift-card purchases at // £500 (50,000 pence) per UTC day. maxUserGiftCardDailyPence = 500_00 // maxAdminGiftCardDailyPence caps the gift-card value an admin can // create/top-up/transfer in one UTC day at £5,000 (500,000 pence). maxAdminGiftCardDailyPence = 500_000 ) // userGiftCardSpentToday returns the total value (in pounds) the user has // spent on ONLINE gift-card purchases so far today, returned as a float64 so // the caller can convert to pence with math.Round, matching the repo's // currency convention. // // Signal: gift_card_transactions rows written by BuyGiftCard — the ONLY // customer-facing online purchase path. Every BuyGiftCard purchase (self and // friend) inserts a row with transaction_type='purchase', reference_type='api' // and user_id = the buyer (see giftcards.go). Admin-created cards // (CreateGiftCard/TopUpGiftCard) also write reference_type='api' but with the // ADMIN's user id, and till sales write reference_type='till_sale', so neither // can match a customer. The payments-based alternative (payments rows with // payment_type='gift_card') does NOT exist in this schema — the payment_type // enum is ('deposit','full','tip','balance','partial') and BuyGiftCard writes // payment_type='full' — so the transactions audit log is the correct signal. // // "Today" is the UTC day boundary (created_at >= CURRENT_DATE), matching the // repo's existing time convention: the DB session runs in timezone=UTC and // completion.go uses the same CURRENT_DATE boundary for its daily loyalty // stamp cap. func userGiftCardSpentToday(ctx context.Context, q db.Querier, userID string) (float64, error) { var spent float64 err := q.QueryRow(ctx, ` SELECT COALESCE(SUM(amount), 0) FROM gift_card_transactions WHERE user_id = $1 AND transaction_type = 'purchase' AND reference_type = 'api' AND created_at >= CURRENT_DATE `, userID).Scan(&spent) if err != nil { return 0, err } return spent, nil } // adminGiftCardValueToday returns the total gift-card value (in pounds) the // admin has created, topped up, transferred, or issued via the till today (UTC // day boundary, created_at >= CURRENT_DATE), returned as a float64 for pence // conversion. This is the SINGLE daily-cap signal shared by the admin API // surface (CreateGiftCard / TopUpGiftCard / TransferGiftCard) AND the till // (CreateTillSale) — an admin surface that otherwise could issue unlimited // balance (MEDIUM-5). // // Signal (chosen to be double-count free across the admin operations): // // 1. Cards the admin created today — SUM(total_funds_added). total_funds_added // is cumulative, so a card created today already reflects any same-day // top-up or transfer INTO it, and its creation amount. This covers cards // created through BOTH the admin API and the till (a till create inserts // the card with created_by = the admin). // 2. API top-ups executed by this admin today on cards created BEFORE today // (cards created today are excluded — term 1 already includes their // funding via total_funds_added, so counting the top-up row again would // double-count). This is the gift_card_transactions rows // (reference_type='api', user_id=admin) written by CreateGiftCard // ('purchase') and TopUpGiftCard ('topup', or 'purchase' on an inventory // card's first top-up). // 3. Till sales executed by this admin today on cards created BEFORE today — // till_sales rows (created_by = admin, status completed/pending — a // pending sale's card was already funded before the Square call). Cards // created today are excluded exactly like term 2, so a till-created card // is counted once via term 1's total_funds_added and a till top-up on an // older card is counted once here. A till sale's gift_card_transactions // row is attributed to the CUSTOMER (reference_type='till_sale'), so it // never enters term 2. // // Transfers INTO pre-existing cards leave no attributable audit row // (TransferGiftCard deliberately writes no gift_card_transactions entry), so // they are not directly counted; a transfer also creates no NEW gift-card // liability, so the daily cap still measures all value this admin has newly // issued today. func adminGiftCardValueToday(ctx context.Context, q db.Querier, adminID string) (float64, error) { var value float64 err := q.QueryRow(ctx, ` SELECT COALESCE(( SELECT SUM(gc.total_funds_added) FROM gift_cards gc WHERE gc.created_by = $1 AND gc.created_at >= CURRENT_DATE ), 0) + COALESCE(( SELECT SUM(gct.amount) FROM gift_card_transactions gct WHERE gct.user_id = $1 AND gct.reference_type = 'api' AND gct.transaction_type IN ('purchase', 'topup') AND gct.created_at >= CURRENT_DATE AND gct.gift_card_id NOT IN ( SELECT gc2.id FROM gift_cards gc2 WHERE gc2.created_by = $1 AND gc2.created_at >= CURRENT_DATE ) ), 0) + COALESCE(( SELECT SUM(ts.total_amount) FROM till_sales ts WHERE ts.created_by = $1 AND ts.status IN ('completed', 'pending') AND ts.created_at >= CURRENT_DATE AND ts.item_id NOT IN ( SELECT gc3.id FROM gift_cards gc3 WHERE gc3.created_by = $1 AND gc3.created_at >= CURRENT_DATE ) ), 0) `, adminID).Scan(&value) if err != nil { return 0, err } return value, nil }