//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 }