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

1124 lines
34 KiB
Go

//go:build test && dev
// +build test,dev
package payments
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"crussell/db"
"crussell/mw"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/go-chi/chi/v5"
)
func resetGiftCardsTestData(t *testing.T) {
t.Helper()
testdb.TruncateTables(t, db.DB)
}
func TestAdminCreateGiftCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
reqBody, _ := json.Marshal(map[string]interface{}{"amount": 50.00})
req := httptest.NewRequest("POST", "/api/admin/gift-cards", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards", CreateGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d", w.Code)
}
var gc GiftCard
if err := json.NewDecoder(w.Body).Decode(&gc); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if gc.TotalFundsAdded != 50.00 || gc.AmountRemaining != 50.00 {
t.Errorf("expected funds and remaining to be 50.00, got added=%.2f remaining=%.2f", gc.TotalFundsAdded, gc.AmountRemaining)
}
}
func TestAdminTopUpGiftCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create gift card
var cardID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
VALUES (50.00, 50.00, $1)
RETURNING id
`, adminID).Scan(&cardID)
if err != nil {
t.Fatalf("failed to insert gift card: %v", err)
}
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 25.00,
"payment_method": "on_the_house",
})
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w.Code, w.Body.String())
}
var gc GiftCard
if err := json.NewDecoder(w.Body).Decode(&gc); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if gc.TotalFundsAdded != 75.00 || gc.AmountRemaining != 75.00 {
t.Errorf("expected topped up card totals, got added=%.2f remaining=%.2f", gc.TotalFundsAdded, gc.AmountRemaining)
}
}
func TestAdminTransferGiftCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create card 1 with £100
var card1ID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
VALUES (100.00, 100.00, $1)
RETURNING id
`, adminID).Scan(&card1ID)
if err != nil {
t.Fatalf("failed to insert card 1: %v", err)
}
// Create card 2 with £20
var card2ID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
VALUES (20.00, 20.00, $1)
RETURNING id
`, adminID).Scan(&card2ID)
if err != nil {
t.Fatalf("failed to insert card 2: %v", err)
}
// Transfer £30 from card 1 to card 2
reqBody, _ := json.Marshal(map[string]interface{}{
"to_card_id": card2ID,
"amount": 30.00,
})
req := httptest.NewRequest("POST", "/api/admin/gift-cards/"+card1ID+"/transfer", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w.Code, w.Body.String())
}
// Verify card 1 has £70 remaining
var card1Remaining float64
err = db.DB.QueryRow(ctx, "SELECT amount_remaining FROM gift_cards WHERE id = $1", card1ID).Scan(&card1Remaining)
if err != nil {
t.Fatalf("failed to query card 1: %v", err)
}
if card1Remaining != 70.00 {
t.Errorf("expected card 1 to have 70.00, got %.2f", card1Remaining)
}
// Verify card 2 has £50 remaining and £50 total funds added
var card2Remaining, card2Added float64
err = db.DB.QueryRow(ctx, "SELECT amount_remaining, total_funds_added FROM gift_cards WHERE id = $1", card2ID).Scan(&card2Remaining, &card2Added)
if err != nil {
t.Fatalf("failed to query card 2: %v", err)
}
if card2Remaining != 50.00 || card2Added != 50.00 {
t.Errorf("expected card 2 to have remaining=50.00 added=50.00, got remaining=%.2f added=%.2f", card2Remaining, card2Added)
}
}
func TestUserRedeemGiftCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
token := jwt.GenerateTestToken(userID, "verified_email")
// Create gift card with £100
var cardID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining)
VALUES (100.00, 100.00)
RETURNING id
`).Scan(&cardID)
if err != nil {
t.Fatalf("failed to insert gift card: %v", err)
}
reqBody, _ := json.Marshal(map[string]interface{}{"code": cardID})
req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/user/giftcards/redeem", RedeemGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w.Code, w.Body.String())
}
// Verify card marked as spent (remaining = 0) and claimed
var amountRemaining float64
var redeemedBy string
err = db.DB.QueryRow(ctx, "SELECT amount_remaining, redeemed_by FROM gift_cards WHERE id = $1", cardID).Scan(&amountRemaining, &redeemedBy)
if err != nil {
t.Fatalf("failed to query gift card: %v", err)
}
if amountRemaining != 0.00 {
t.Errorf("expected card to be spent, got remaining=%.2f", amountRemaining)
}
if redeemedBy != userID {
t.Errorf("expected card redeemed_by to be user, got '%s'", redeemedBy)
}
// Verify balance added to user
var balance float64
err = db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
if err != nil {
t.Fatalf("failed to query user balance: %v", err)
}
if balance != 100.00 {
t.Errorf("expected user balance to be 100.00, got %.2f", balance)
}
}
func TestBuyGiftCard_Self(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
token := jwt.GenerateTestToken(userID, "verified_email")
// Charge a mock payment token
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 2000, // £20.00 in cents
"recipient_type": "self",
"new_card_token": "cnon:card-nonce-ok",
"idempotency_key": "idempotency-key-buy-gc-self",
})
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/user/giftcards/buy", BuyGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. Body: %s", w.Code, w.Body.String())
}
// Verify user balance is now £20.00
var balance float64
err = db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
if err != nil {
t.Fatalf("failed to query user balance: %v", err)
}
if balance != 20.00 {
t.Errorf("expected user balance 20.00, got %.2f", balance)
}
// Verify purchase payment record was created
var payCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE created_by = $1", userID).Scan(&payCount)
if err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if payCount != 1 {
t.Errorf("expected 1 payment record, got %d", payCount)
}
}
func TestBuyGiftCard_Friend(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
token := jwt.GenerateTestToken(userID, "verified_email")
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 5000, // £50.00 in cents
"recipient_type": "friend",
"new_card_token": "cnon:card-nonce-ok",
"idempotency_key": "idempotency-key-buy-gc-friend",
})
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/user/giftcards/buy", BuyGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. Body: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.NewDecoder(w.Body).Decode(&resp)
cardID := resp["code"].(string)
// Verify card was created with £50.00 remaining (stays active, unredeemed)
var remaining, added float64
var redeemedBy sql.NullString
err = db.DB.QueryRow(ctx, "SELECT amount_remaining, total_funds_added, redeemed_by FROM gift_cards WHERE id = $1", cardID).Scan(&remaining, &added, &redeemedBy)
if err != nil {
t.Fatalf("failed to query card: %v", err)
}
if remaining != 50.00 || added != 50.00 {
t.Errorf("expected card values to be 50.00, got remaining=%.2f added=%.2f", remaining, added)
}
if redeemedBy.Valid {
t.Errorf("expected card redeemed_by to be null, got '%s'", redeemedBy.String)
}
}
func TestAdminRecordPayment_CashAndGiftCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
bookingID, err := fixtures.CreateTestBooking(db.DB, adminID, serviceID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
// Update booking to in_progress so it is payable
_, _ = db.DB.Exec(ctx, "UPDATE bookings SET status = 'in_progress' WHERE id = $1", bookingID)
// Create a physical gift card code with £100 balance
var cardID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining)
VALUES (100.00, 100.00)
RETURNING id
`).Scan(&cardID)
if err != nil {
t.Fatalf("failed to create gift card: %v", err)
}
// 1. Pay £30 with CASH
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 3000, // £30.00 in cents
"payment_type": "full",
"payment_method": "cash",
})
req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/payment", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/bookings/{id}/payment", CreateTerminalPayment)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w.Code, w.Body.String())
}
// Verify cash payment recorded
var cashPayCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND payment_method = 'cash'", bookingID).Scan(&cashPayCount)
if err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if cashPayCount != 1 {
t.Errorf("expected 1 cash payment, got %d", cashPayCount)
}
// 2. Pay £40 with PHYSICAL GIFT CARD (guest checkout simulation)
reqBody2, _ := json.Marshal(map[string]interface{}{
"amount": 4000, // £40.00 in cents
"payment_type": "full",
"payment_method": "giftcard",
"gift_card_id": cardID,
})
req2 := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/payment", bytes.NewBuffer(reqBody2))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
w2 := httptest.NewRecorder()
r2 := chi.NewRouter()
r2.Use(mw.RequireAuth)
r2.Post("/api/admin/bookings/{id}/payment", CreateTerminalPayment)
r2.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w2.Code, w2.Body.String())
}
// Verify gift card balance deducted from card directly
var remaining float64
err = db.DB.QueryRow(ctx, "SELECT amount_remaining FROM gift_cards WHERE id = $1", cardID).Scan(&remaining)
if err != nil {
t.Fatalf("failed to query card: %v", err)
}
if remaining != 60.00 {
t.Errorf("expected gift card balance to be 60.00, got %.2f", remaining)
}
// Verify gift card payment record created
var gcPayCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND payment_method = 'giftcard'", bookingID).Scan(&gcPayCount)
if err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if gcPayCount != 1 {
t.Errorf("expected 1 gift card payment, got %d", gcPayCount)
}
// 3. Redeem remaining £60 of gift card to user account
// Setup user account with some balance first
_, _ = db.DB.Exec(ctx, "INSERT INTO user_giftcard_balances (user_id, balance) VALUES ($1, 60.00)", adminID)
// Now pay £25 using user account balance
reqBody3, _ := json.Marshal(map[string]interface{}{
"amount": 2500, // £25.00 in cents
"payment_type": "full",
"payment_method": "giftcard",
})
req3 := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/payment", bytes.NewBuffer(reqBody3))
req3.Header.Set("Authorization", "Bearer "+token)
req3.Header.Set("Content-Type", "application/json")
w3 := httptest.NewRecorder()
r3 := chi.NewRouter()
r3.Use(mw.RequireAuth)
r3.Post("/api/admin/bookings/{id}/payment", CreateTerminalPayment)
r3.ServeHTTP(w3, req3)
if w3.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. Body: %s", w3.Code, w3.Body.String())
}
// Verify user account balance was deducted
var userBalance float64
err = db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", adminID).Scan(&userBalance)
if err != nil {
t.Fatalf("failed to query user balance: %v", err)
}
if userBalance != 35.00 {
t.Errorf("expected user balance to be 35.00, got %.2f", userBalance)
}
}
// --- New Tests for branch features ---
func TestGetExpiredBalances(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Seed expired balances
for i := 0; i < 2; i++ {
_, err = db.DB.Exec(ctx, `
INSERT INTO gift_card_expired_balances (account_id, original_balance, expired_at)
VALUES ($1, $2, NOW() - interval '30 days')
`, adminID, float64(25.00*(i+1)))
if err != nil {
t.Fatalf("failed to seed expired balance %d: %v", i, err)
}
}
req := httptest.NewRequest("GET", "/api/admin/gift-cards/expired-balances", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Get("/api/admin/gift-cards/expired-balances", GetExpiredBalances)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
total, ok := resp["total"].(float64)
if !ok || total != 2 {
t.Errorf("expected total 2, got %v", resp["total"])
}
balances, ok := resp["expired_balances"].([]interface{})
if !ok || len(balances) != 2 {
t.Errorf("expected 2 expired_balances, got %d", len(balances))
}
}
func TestClaimExpiredBalance_HappyPath(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Seed an expired balance with known ID
var balanceID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_card_expired_balances (account_id, original_balance, expired_at)
VALUES ($1, 50.00, NOW() - interval '30 days')
RETURNING id
`, adminID).Scan(&balanceID)
if err != nil {
t.Fatalf("failed to seed expired balance: %v", err)
}
notes := "claimed via test"
reqBody, _ := json.Marshal(map[string]interface{}{
"balance_id": balanceID,
"notes": notes,
})
req := httptest.NewRequest("POST", "/api/admin/gift-cards/expired-balances/claim", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards/expired-balances/claim", ClaimExpiredBalance)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d. Body: %s", w.Code, w.Body.String())
}
var resp map[string]string
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if resp["status"] != "claimed" {
t.Errorf("expected status 'claimed', got '%s'", resp["status"])
}
// Verify claimed_at is set in DB
var claimedAt sql.NullTime
err = db.DB.QueryRow(ctx, "SELECT claimed_at FROM gift_card_expired_balances WHERE id = $1", balanceID).Scan(&claimedAt)
if err != nil {
t.Fatalf("failed to query expired balance: %v", err)
}
if !claimedAt.Valid {
t.Error("expected claimed_at to be set, got null")
}
}
func TestClaimExpiredBalance_AlreadyClaimed(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Seed an expired balance that is already claimed
var balanceID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_card_expired_balances (account_id, original_balance, expired_at, claimed_at, claimed_by_admin)
VALUES ($1, 50.00, NOW() - interval '30 days', NOW(), $2)
RETURNING id
`, adminID, adminID).Scan(&balanceID)
if err != nil {
t.Fatalf("failed to seed claimed expired balance: %v", err)
}
reqBody, _ := json.Marshal(map[string]interface{}{
"balance_id": balanceID,
})
req := httptest.NewRequest("POST", "/api/admin/gift-cards/expired-balances/claim", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards/expired-balances/claim", ClaimExpiredBalance)
r.ServeHTTP(w, req)
if w.Code != http.StatusConflict {
t.Errorf("expected 409, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestClaimExpiredBalance_NotFound(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
reqBody, _ := json.Marshal(map[string]interface{}{
"balance_id": "aaaaaaaaaaaa",
})
req := httptest.NewRequest("POST", "/api/admin/gift-cards/expired-balances/claim", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards/expired-balances/claim", ClaimExpiredBalance)
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestGetGiftCards_Pagination(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create 15 gift cards
for i := 0; i < 15; i++ {
_, err = db.DB.Exec(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
VALUES (10.00, 10.00, $1)
`, adminID)
if err != nil {
t.Fatalf("failed to create gift card %d: %v", i, err)
}
}
// Request page 1 with per_page=5
req := httptest.NewRequest("GET", "/api/admin/gift-cards?page=1&per_page=5", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Get("/api/admin/gift-cards", GetGiftCards)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d. Body: %s", w.Code, w.Body.String())
}
var resp GiftCardListResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(resp.GiftCards) != 5 {
t.Errorf("expected 5 gift cards on page 1, got %d", len(resp.GiftCards))
}
if resp.Total != 15 {
t.Errorf("expected total 15, got %d", resp.Total)
}
if resp.Page != 1 {
t.Errorf("expected page 1, got %d", resp.Page)
}
if resp.PerPage != 5 {
t.Errorf("expected perPage 5, got %d", resp.PerPage)
}
if resp.TotalPages != 3 {
t.Errorf("expected totalPages 3, got %d", resp.TotalPages)
}
// Request page 3 to verify last page
req3 := httptest.NewRequest("GET", "/api/admin/gift-cards?page=3&per_page=5", nil)
req3.Header.Set("Authorization", "Bearer "+token)
w3 := httptest.NewRecorder()
r3 := chi.NewRouter()
r3.Use(mw.RequireAuth)
r3.Get("/api/admin/gift-cards", GetGiftCards)
r3.ServeHTTP(w3, req3)
if w3.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w3.Code)
}
var resp3 GiftCardListResponse
if err := json.NewDecoder(w3.Body).Decode(&resp3); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(resp3.GiftCards) != 5 {
t.Errorf("expected 5 gift cards on page 3, got %d", len(resp3.GiftCards))
}
if resp3.Total != 15 {
t.Errorf("expected total 15 on page 3, got %d", resp3.Total)
}
}
func TestGetGiftCards_Search(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create cards with specific hex IDs for search predictability
searchableID := "aaaaaabbbbcc"
nonSearchableID := "ddddeeeeffff"
_, err = db.DB.Exec(ctx, `
INSERT INTO gift_cards (id, total_funds_added, amount_remaining, created_by)
VALUES ($1, 10.00, 10.00, $2)
`, searchableID, adminID)
if err != nil {
t.Fatalf("failed to create searchable card: %v", err)
}
_, err = db.DB.Exec(ctx, `
INSERT INTO gift_cards (id, total_funds_added, amount_remaining, created_by)
VALUES ($1, 20.00, 20.00, $2)
`, nonSearchableID, adminID)
if err != nil {
t.Fatalf("failed to create non-searchable card: %v", err)
}
// Search by partial ID match
req := httptest.NewRequest("GET", "/api/admin/gift-cards?q=aaaa", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Get("/api/admin/gift-cards", GetGiftCards)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d. Body: %s", w.Code, w.Body.String())
}
var resp GiftCardListResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if resp.Total != 1 {
t.Errorf("expected total 1, got %d", resp.Total)
}
if len(resp.GiftCards) != 1 {
t.Errorf("expected 1 gift card in results, got %d", len(resp.GiftCards))
}
if len(resp.GiftCards) > 0 && resp.GiftCards[0].ID != searchableID {
t.Errorf("expected card ID %s, got %s", searchableID, resp.GiftCards[0].ID)
}
}
func TestCreateGiftCard_NegativeAmount(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
reqBody, _ := json.Marshal(map[string]interface{}{"amount": -10.00})
req := httptest.NewRequest("POST", "/api/admin/gift-cards", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards", CreateGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for negative amount, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestCreateGiftCard_ZeroAmountNoInventory(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
reqBody, _ := json.Marshal(map[string]interface{}{"amount": 0, "is_inventory": false})
req := httptest.NewRequest("POST", "/api/admin/gift-cards", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards", CreateGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for zero amount without inventory, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestCreateGiftCard_ZeroAmountInventory(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
reqBody, _ := json.Marshal(map[string]interface{}{"amount": 0, "is_inventory": true})
req := httptest.NewRequest("POST", "/api/admin/gift-cards", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Post("/api/admin/gift-cards", CreateGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected 201 for inventory card with zero amount, got %d. Body: %s", w.Code, w.Body.String())
}
var gc GiftCard
if err := json.NewDecoder(w.Body).Decode(&gc); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if !gc.IsInventory {
t.Error("expected card to be inventory card")
}
if gc.TotalFundsAdded != 0 || gc.AmountRemaining != 0 {
t.Errorf("expected zero balance card, got added=%.2f remaining=%.2f", gc.TotalFundsAdded, gc.AmountRemaining)
}
}
func TestTopUpGiftCard_InvalidPaymentMethod(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create gift card
var cardID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
VALUES (50.00, 50.00, $1)
RETURNING id
`, adminID).Scan(&cardID)
if err != nil {
t.Fatalf("failed to insert gift card: %v", err)
}
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 25.00,
"payment_method": "invalid_method",
})
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for invalid payment method, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestTopUpGiftCard_RedeemedCard(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
adminID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
_, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
token := jwt.GenerateTestToken(adminID, "admin")
// Create a card that's already redeemed
var cardID string
err = db.DB.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by)
VALUES (50.00, 0, $1, NOW(), $1)
RETURNING id
`, adminID).Scan(&cardID)
if err != nil {
t.Fatalf("failed to insert redeemed gift card: %v", err)
}
reqBody, _ := json.Marshal(map[string]interface{}{
"amount": 25.00,
"payment_method": "on_the_house",
})
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r := chi.NewRouter()
r.Use(mw.RequireAuth)
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for redeemed card topup, got %d. Body: %s", w.Code, w.Body.String())
}
}
func TestBuyGiftCard_Idempotency(t *testing.T) {
resetGiftCardsTestData(t)
ctx := context.Background()
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
token := jwt.GenerateTestToken(userID, "verified_email")
idempotencyKey := "idempotent-buy-gc-test"
reqBody := map[string]interface{}{
"amount": 2000,
"recipient_type": "self",
"new_card_token": "cnon:card-nonce-ok",
"idempotency_key": idempotencyKey,
}
// Send request
body1, _ := json.Marshal(reqBody)
req1 := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(body1))
req1.Header.Set("Authorization", "Bearer "+token)
req1.Header.Set("Content-Type", "application/json")
w1 := httptest.NewRecorder()
r1 := chi.NewRouter()
r1.Use(mw.RequireAuth)
r1.Post("/api/user/giftcards/buy", BuyGiftCard)
r1.ServeHTTP(w1, req1)
if w1.Code != http.StatusCreated && w1.Code != http.StatusOK {
t.Errorf("buy request: expected 201 or 200, got %d. Body: %s", w1.Code, w1.Body.String())
}
// Verify exactly one payment record was created for this key
var payCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE idempotency_key = $1", idempotencyKey).Scan(&payCount)
if err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if payCount != 1 {
t.Errorf("expected 1 payment record for idempotency key, got %d", payCount)
}
// Verify exactly one user balance record
var balCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balCount)
if err != nil {
t.Fatalf("failed to query user balances: %v", err)
}
if balCount != 1 {
t.Errorf("expected 1 user balance record, got %d", balCount)
}
// Verify exactly one gift card was created for self-purchase (amount_remaining=0, redeemed)
var cardCount int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards WHERE redeemed_by = $1", userID).Scan(&cardCount)
if err != nil {
t.Fatalf("failed to query gift cards: %v", err)
}
if cardCount != 1 {
t.Errorf("expected 1 gift card record, got %d", cardCount)
}
// Send second request with same idempotency key
body2, _ := json.Marshal(reqBody)
req2 := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(body2))
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
w2 := httptest.NewRecorder()
r2 := chi.NewRouter()
r2.Use(mw.RequireAuth)
r2.Post("/api/user/giftcards/buy", BuyGiftCard)
r2.ServeHTTP(w2, req2)
// Verify counts remain unchanged (idempotent)
var payCount2 int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE idempotency_key = $1", idempotencyKey).Scan(&payCount2)
if err != nil {
t.Fatalf("failed to query payments: %v", err)
}
if payCount2 != 1 {
t.Errorf("after second request: expected 1 payment record, got %d", payCount2)
}
var balCount2 int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balCount2)
if err != nil {
t.Fatalf("failed to query user balances: %v", err)
}
if balCount2 != 1 {
t.Errorf("after second request: expected 1 user balance record, got %d", balCount2)
}
var cardCount2 int
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards WHERE redeemed_by = $1", userID).Scan(&cardCount2)
if err != nil {
t.Fatalf("failed to query gift cards: %v", err)
}
if cardCount2 != 1 {
t.Errorf("after second request: expected 1 gift card record, got %d", cardCount2)
}
}