Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
380 lines
13 KiB
Go
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
|
|
} |