Files
Crussell/backend/handlers/payments/giftcard_limits_test.go
T

380 lines
13 KiB
Go

//go:build test && dev
package payments
import (
"context"
"testing"
"time"
"crussell/clock"
"crussell/db"
"crussell/testutils"
"crussell/testutils/fixtures"
"github.com/stretchr/testify/require"
)
// ===========================================================================
// userGiftCardSpentToday
// ===========================================================================
func TestUserGiftCardSpentToday_ZeroRows(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.Equal(t, float64(0), spent, "expected 0 when no gift_card_transactions exist")
}
func TestUserGiftCardSpentToday_SinglePurchase(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
// Create a purchase row with today's date
gcID := insertGiftCard(ctx, t, tx, nil)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", 100.00)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.InDelta(t, 100.00, spent, 0.005, "expected £100.00 from single purchase")
}
func TestUserGiftCardSpentToday_MultiplePurchases(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
gcID := insertGiftCard(ctx, t, tx, nil)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", 100.00)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", 200.00)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", 50.00)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.InDelta(t, 350.00, spent, 0.005, "expected £350.00 from 3 purchases")
}
func TestUserGiftCardSpentToday_CapBoundary_Exactly(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
// maxUserGiftCardDailyPence = 500_00 → £500.00
capPounds := float64(maxUserGiftCardDailyPence) / 100.0
gcID := insertGiftCard(ctx, t, tx, nil)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", capPounds)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.InDelta(t, capPounds, spent, 0.005, "expected exactly the cap at £%.2f", capPounds)
}
func TestUserGiftCardSpentToday_OnePennyOverCap(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
// One penny over the daily cap
capPounds := float64(maxUserGiftCardDailyPence) / 100.0
over := capPounds + 0.01
gcID := insertGiftCard(ctx, t, tx, nil)
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", over)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.InDelta(t, over, spent, 0.005, "expected £%.2f (one penny over cap)", over)
require.True(t, spent > capPounds, "expected spent %.2f to exceed cap %.2f", spent, capPounds)
}
func TestUserGiftCardSpentToday_FiltersTransactionType(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
gcID := insertGiftCard(ctx, t, tx, nil)
// Only 'purchase' should be counted — insert a 'topup' row that must be excluded
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "topup", "api", 500.00)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.Equal(t, float64(0), spent, "expected 0 — 'topup' rows are not purchases")
}
func TestUserGiftCardSpentToday_FiltersReferenceType(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
gcID := insertGiftCard(ctx, t, tx, nil)
// Only 'api' reference type should be counted — 'till_sale' must be excluded
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "till_sale", 200.00)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.Equal(t, float64(0), spent, "expected 0 — 'till_sale' reference_type is excluded")
}
func TestUserGiftCardSpentToday_DifferentUserNotCounted(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
userID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
otherID, err := fixtures.CreateTestUser(tx)
require.NoError(t, err)
gcID := insertGiftCard(ctx, t, tx, nil)
// Other user's purchase should not count toward this user
insertGiftCardTransaction(ctx, t, tx, gcID, userID, "purchase", "api", 100.00)
insertGiftCardTransaction(ctx, t, tx, gcID, otherID, "purchase", "api", 300.00)
spent, err := userGiftCardSpentToday(ctx, tx, userID)
require.NoError(t, err)
require.InDelta(t, 100.00, spent, 0.005, "expected only this user's purchases")
}
// ===========================================================================
// adminGiftCardValueToday
// ===========================================================================
func TestAdminGiftCardValueToday_ZeroRows(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.Equal(t, float64(0), value, "expected 0 when no gift cards or transactions exist")
}
func TestAdminGiftCardValueToday_CardsCreatedToday(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// Term 1: cards created today by this admin
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 200.00})
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 300.00})
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, 500.00, value, 0.005, "expected £500 from two cards created today")
}
func TestAdminGiftCardValueToday_TopUpOnPreExistingCard(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// Card created YESTERDAY (created_at = -48h) so it's NOT counted in term 1
gcID := insertGiftCardAt(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 100.00}, clock.Now().Add(-48*time.Hour))
// Topup today on that pre-existing card — counted in term 2
insertGiftCardTransaction(ctx, t, tx, gcID, adminID, "topup", "api", 50.00)
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, 50.00, value, 0.005, "expected £50 from topup on pre-existing card")
}
func TestAdminGiftCardValueToday_TillSaleOnPreExistingCard(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// Card created YESTERDAY so it's not counted in term 1
gcID := insertGiftCardAt(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 100.00}, clock.Now().Add(-48*time.Hour))
// Till sale today on that pre-existing card — counted in term 3
insertTillSale(ctx, t, tx, adminID, gcID, 75.00, "completed")
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, 75.00, value, 0.005, "expected £75 from till sale on pre-existing card")
}
func TestAdminGiftCardValueToday_CombinedSources(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// Term 1: card created today by admin
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 200.00})
// Pre-existing card (created yesterday) + topup today (term 2)
gcPre := insertGiftCardAt(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 100.00}, clock.Now().Add(-48*time.Hour))
insertGiftCardTransaction(ctx, t, tx, gcPre, adminID, "topup", "api", 50.00)
// Another pre-existing card + till sale today (term 3)
gcPre2 := insertGiftCardAt(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 100.00}, clock.Now().Add(-48*time.Hour))
insertTillSale(ctx, t, tx, adminID, gcPre2, 25.00, "completed")
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, 275.00, value, 0.005, "expected £275 from all three sources combined")
}
func TestAdminGiftCardValueToday_DoubleCountPrevention(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// Card created today with total_funds_added=200. This is counted in term 1.
// A topup AND till sale on that SAME card (created today) must NOT also count
// in terms 2 and 3 (the NOT IN subqueries exclude today-created cards).
gcID := insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 200.00})
insertGiftCardTransaction(ctx, t, tx, gcID, adminID, "topup", "api", 50.00)
insertTillSale(ctx, t, tx, adminID, gcID, 25.00, "completed")
value, err := adminGiftCardValueToday(ctx, tx, adminID)
// Only term 1 should count: the 200 from total_funds_added.
// The topup and till_sale are on today's card, so they're double-count protected.
require.NoError(t, err)
require.InDelta(t, 200.00, value, 0.005, "expected only £200 from term 1; topup and sale must be excluded")
}
func TestAdminGiftCardValueToday_CapBoundary(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
// maxAdminGiftCardDailyPence = 500_000 → £5,000
capPounds := float64(maxAdminGiftCardDailyPence) / 100.0
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: capPounds})
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, capPounds, value, 0.005, "expected exactly the admin cap at £%.2f", capPounds)
}
func TestAdminGiftCardValueToday_OnePennyOverCap(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
capPounds := float64(maxAdminGiftCardDailyPence) / 100.0
over := capPounds + 0.01
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: over})
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, over, value, 0.005, "expected £%.2f (one penny over cap)", over)
require.True(t, value > capPounds, "expected value %.2f to exceed cap %.2f", value, capPounds)
}
func TestAdminGiftCardValueToday_IgnoresOtherAdmins(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
otherAdmin, err := fixtures.CreateTestAdminUser(tx)
require.NoError(t, err)
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: adminID, totalFunds: 100.00})
insertGiftCard(ctx, t, tx, &giftCardOptions{createdBy: otherAdmin, totalFunds: 500.00})
value, err := adminGiftCardValueToday(ctx, tx, adminID)
require.NoError(t, err)
require.InDelta(t, 100.00, value, 0.005, "expected only this admin's card value")
}
// ===========================================================================
// Test helpers — build test data with minimal column requirements
// ===========================================================================
type giftCardOptions struct {
createdBy string
totalFunds float64
}
func insertGiftCard(ctx context.Context, t *testing.T, q db.Querier, opts *giftCardOptions) string {
t.Helper()
return insertGiftCardAt(ctx, t, q, opts, clock.Now())
}
func insertGiftCardAt(ctx context.Context, t *testing.T, q db.Querier, opts *giftCardOptions, createdAt time.Time) string {
t.Helper()
if opts == nil {
opts = &giftCardOptions{}
}
// created_by is nullable, pass nil when empty
if opts.createdBy != "" {
var id string
err := q.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, created_at)
VALUES ($1, $1, $2, $3)
RETURNING id
`, opts.totalFunds, opts.createdBy, createdAt).Scan(&id)
require.NoError(t, err)
return id
}
var id string
err := q.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_at)
VALUES ($1, $1, $2)
RETURNING id
`, opts.totalFunds, createdAt).Scan(&id)
require.NoError(t, err)
return id
}
func insertGiftCardTransaction(ctx context.Context, t *testing.T, q db.Querier, giftCardID, userID, txType, refType string, amount float64) string {
t.Helper()
var id string
err := q.QueryRow(ctx, `
INSERT INTO gift_card_transactions (gift_card_id, transaction_type, amount, reference_type, user_id, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
RETURNING id
`, giftCardID, txType, amount, refType, userID).Scan(&id)
require.NoError(t, err)
return id
}
func insertTillSale(ctx context.Context, t *testing.T, q db.Querier, adminID, itemID string, totalAmount float64, status string) string {
t.Helper()
var id string
err := q.QueryRow(ctx, `
INSERT INTO till_sales (item_type, item_id, description, quantity, unit_price, total_amount, payment_method, status, created_by, created_at, updated_at)
VALUES ('gift_card', $1, 'test till sale', 1, $2, $2, 'online_square', $3, $4, NOW(), NOW())
RETURNING id
`, itemID, totalAmount, status, adminID).Scan(&id)
require.NoError(t, err)
return id
}