//go:build test && dev package payments import ( "bytes" "context" "database/sql" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "crussell/db" "crussell/mw" "crussell/testutils" "crussell/testutils/fixtures" "crussell/testutils/jwt" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" ) func TestAdminCreateGiftCard(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create gift card var cardID string err = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) 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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) 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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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_TransactionFailure_SkipsSquare(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") // Create a cancelled context so the nested transaction fails cancelCtx, cancel := context.WithCancel(ctx) cancel() reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 2000, "recipient_type": "self", "new_card_token": "cnon:card-nonce-ok", "idempotency_key": "idempotency-key-buy-gc-txn-fail", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(cancelCtx, tx.(pgx.Tx))) 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.StatusInternalServerError { t.Errorf("expected status 500 due to cancelled context, got %d. Body: %s", w.Code, w.Body.String()) } // Verify no completed payment records exist — confirming Square was never called var payCount int err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE created_by = $1 AND status = 'completed'", userID).Scan(&payCount) if err != nil { t.Fatalf("failed to query payments: %v", err) } if payCount != 0 { t.Errorf("expected 0 completed payment records (Square should not have been called), got %d", payCount) } } func TestBuyGiftCard_Friend(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) 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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") serviceID, err := fixtures.CreateTestService(tx) if err != nil { t.Fatalf("failed to create service: %v", err) } bookingID, err := fixtures.CreateTestBooking(tx, adminID, serviceID) if err != nil { t.Fatalf("failed to create booking: %v", err) } // Update booking to in_progress so it is payable _, _ = tx.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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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") req2 = req2.WithContext(db.ContextWithTx(req2.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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 _, _ = tx.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") req3 = req3.WithContext(db.ContextWithTx(req3.Context(), tx.(pgx.Tx))) 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 = tx.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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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) req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 = tx.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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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) req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) req3 = req3.WithContext(db.ContextWithTx(req3.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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 = tx.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) req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create gift card var cardID string err = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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 = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) 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") req1 = req1.WithContext(db.ContextWithTx(req1.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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 = tx.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") req2 = req2.WithContext(db.ContextWithTx(req2.Context(), tx.(pgx.Tx))) 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 = tx.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 = tx.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 = tx.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) } } // TestAdminCreateGiftCard_ExpiryDateIsNull verifies that gift cards created via // CreateGiftCard no longer have expiry_date set (rolling 24-month expiry via last_used_at). func TestAdminCreateGiftCard_ExpiryDateIsNull(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.Fatalf("expected 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) } // Verify expiry_date is NULL in the database var expiryDate *time.Time err = tx.QueryRow(ctx, `SELECT expiry_date FROM gift_cards WHERE id = $1`, gc.ID).Scan(&expiryDate) if err != nil { t.Fatalf("failed to query gift card expiry_date: %v", err) } if expiryDate != nil { t.Error("expected expiry_date to be NULL for rolling-expiry gift cards") } } // TestGetGiftCards_InventoryFilter verifies the ?type=customer|inventory query parameter. func TestGetGiftCards_InventoryFilter(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Insert one customer card and one inventory card _, err = tx.Exec(ctx, `INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) VALUES (10.00, 10.00, $1, FALSE)`, adminID) if err != nil { t.Fatalf("failed to create customer card: %v", err) } _, err = tx.Exec(ctx, `INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) VALUES (20.00, 20.00, $1, TRUE)`, adminID) if err != nil { t.Fatalf("failed to create inventory card: %v", err) } // Test ?type=customer filter reqCustomer := httptest.NewRequest("GET", "/api/admin/gift-cards?type=customer", nil) reqCustomer.Header.Set("Authorization", "Bearer "+token) reqCustomer = reqCustomer.WithContext(db.ContextWithTx(reqCustomer.Context(), tx.(pgx.Tx))) wCustomer := httptest.NewRecorder() r := chi.NewRouter() r.Use(mw.RequireAuth) r.Get("/api/admin/gift-cards", GetGiftCards) r.ServeHTTP(wCustomer, reqCustomer) if wCustomer.Code != http.StatusOK { t.Fatalf("expected 200, got %d", wCustomer.Code) } var respCustomer GiftCardListResponse if err := json.NewDecoder(wCustomer.Body).Decode(&respCustomer); err != nil { t.Fatalf("failed to decode customer response: %v", err) } customerCount := 0 inventoryCount := 0 for _, gc := range respCustomer.GiftCards { if gc.IsInventory { inventoryCount++ } else { customerCount++ } } if respCustomer.Total != 1 { t.Errorf("expected total 1 (customer cards only), got %d", respCustomer.Total) } if customerCount != 1 { t.Errorf("expected 1 customer card in filtered list, got %d", customerCount) } if inventoryCount != 0 { t.Errorf("expected 0 inventory cards in customer filter, got %d", inventoryCount) } // Test ?type=inventory filter reqInventory := httptest.NewRequest("GET", "/api/admin/gift-cards?type=inventory", nil) reqInventory.Header.Set("Authorization", "Bearer "+token) reqInventory = reqInventory.WithContext(db.ContextWithTx(reqInventory.Context(), tx.(pgx.Tx))) wInventory := httptest.NewRecorder() rInventory := chi.NewRouter() rInventory.Use(mw.RequireAuth) rInventory.Get("/api/admin/gift-cards", GetGiftCards) rInventory.ServeHTTP(wInventory, reqInventory) if wInventory.Code != http.StatusOK { t.Fatalf("expected 200, got %d", wInventory.Code) } var respInventory GiftCardListResponse if err := json.NewDecoder(wInventory.Body).Decode(&respInventory); err != nil { t.Fatalf("failed to decode inventory response: %v", err) } if respInventory.Total != 1 { t.Errorf("expected total 1 (inventory cards only), got %d", respInventory.Total) } for _, gc := range respInventory.GiftCards { if !gc.IsInventory { t.Errorf("expected only inventory cards, got customer card %s", gc.ID) } } // Test no filter (should return both) reqAll := httptest.NewRequest("GET", "/api/admin/gift-cards", nil) reqAll.Header.Set("Authorization", "Bearer "+token) reqAll = reqAll.WithContext(db.ContextWithTx(reqAll.Context(), tx.(pgx.Tx))) wAll := httptest.NewRecorder() rAll := chi.NewRouter() rAll.Use(mw.RequireAuth) rAll.Get("/api/admin/gift-cards", GetGiftCards) rAll.ServeHTTP(wAll, reqAll) if wAll.Code != http.StatusOK { t.Fatalf("expected 200, got %d", wAll.Code) } var respAll GiftCardListResponse if err := json.NewDecoder(wAll.Body).Decode(&respAll); err != nil { t.Fatalf("failed to decode all response: %v", err) } if respAll.Total != 2 { t.Errorf("expected total 2 (all cards), got %d", respAll.Total) } } // ============================================================================= // GetGiftCardBalance — GET /api/user/giftcards/balance // ============================================================================= func TestGetGiftCardBalance_HappyPath(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } _, err = tx.Exec(ctx, `INSERT INTO user_giftcard_balances (user_id, balance) VALUES ($1, 75.50)`, userID) if err != nil { t.Fatalf("failed to insert balance: %v", err) } req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil) reqCtx := context.WithValue(req.Context(), mw.UserIDKey, userID) reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email") reqCtx = db.ContextWithTx(reqCtx, tx.(pgx.Tx)) req = req.WithContext(reqCtx) w := httptest.NewRecorder() GetGiftCardBalance(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var resp map[string]float64 if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["balance"] != 75.50 { t.Errorf("expected balance 75.50, got %.2f", resp["balance"]) } } func TestGetGiftCardBalance_NoBalance(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil) reqCtx := context.WithValue(req.Context(), mw.UserIDKey, userID) reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email") req = req.WithContext(reqCtx) w := httptest.NewRecorder() GetGiftCardBalance(w, req) if w.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var resp map[string]float64 if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["balance"] != 0.00 { t.Errorf("expected balance 0.00, got %.2f", resp["balance"]) } } func TestGetGiftCardBalance_Unauthenticated(t *testing.T) { _, _ = testutils.SetupTestTx(t) req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil) w := httptest.NewRecorder() GetGiftCardBalance(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // TransferGiftCard — Additional edge cases // ============================================================================= func TestTransferGiftCard_SameCardRejected(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create a gift card. var cardID string err = tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by) VALUES (100.00, 100.00, $1) RETURNING id `, adminID).Scan(&cardID) if err != nil { t.Fatalf("failed to create gift card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": cardID, "amount": 30.00, }) req := httptest.NewRequest("POST", "/api/admin/gift-cards/"+cardID+"/transfer", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400 for same-card transfer, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_SourceNotFound(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create a destination card. var card2ID string err = tx.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 create destination card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": card2ID, "amount": 10.00, }) req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusNotFound { t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_DestinationNotFound(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create a source card. var card1ID string err = tx.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 create source card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": "bbbbbbbbbbbb", "amount": 10.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusNotFound { t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_InsufficientBalance(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") var card1ID, card2ID string err = tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by) VALUES (10.00, 10.00, $1) RETURNING id `, adminID).Scan(&card1ID) if err != nil { t.Fatalf("failed to create source card: %v", err) } err = tx.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 create destination card: %v", err) } // Try to transfer more than available. reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": card2ID, "amount": 50.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // RedeemGiftCard — Additional edge cases // ============================================================================= func TestRedeemGiftCard_AlreadyRedeemed(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") // Create a gift card that is already redeemed. var cardID string err = tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, redeemed_by, redeemed_at) VALUES (100.00, 0, $1, NOW()) RETURNING id `, userID).Scan(&cardID) if err != nil { t.Fatalf("failed to create redeemed 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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestRedeemGiftCard_NotFound(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{"code": "cccccccccccc"}) req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusNotFound { t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String()) } } func TestRedeemGiftCard_InvalidCode(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{"code": "$$$"}) req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestRedeemGiftCard_ZeroBalance(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") // Create a gift card with zero remaining balance (but not redeemed). var cardID string err = tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining) VALUES (0, 0) RETURNING id `).Scan(&cardID) if err != nil { t.Fatalf("failed to create zero-balance 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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // TopUpGiftCard — Additional edge cases // ============================================================================= func TestTopUpGiftCard_NotFound(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 25.00, "payment_method": "on_the_house", }) req := httptest.NewRequest("PUT", "/api/admin/gift-cards/dddddddddddd/topup", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusNotFound { t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String()) } } func TestTopUpGiftCard_NegativeAmount(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") var cardID string err = tx.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 create gift card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "amount": -10.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestTopUpGiftCard_ZeroAmount(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") var cardID string err = tx.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 create gift card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 0, "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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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 status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestTopUpGiftCard_InventoryCardFirstTopUp(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") // Create an inventory card with zero balance. var cardID string err = tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) VALUES (0, 0, $1, TRUE) RETURNING id `, adminID).Scan(&cardID) if err != nil { t.Fatalf("failed to create inventory card: %v", err) } reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 30.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") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.Fatalf("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 != 30.00 || gc.AmountRemaining != 30.00 { t.Errorf("expected added and remaining 30.00, got added=%.2f remaining=%.2f", gc.TotalFundsAdded, gc.AmountRemaining) } // Verify transaction type is 'purchase' for first top-up on inventory card. var txType string err = tx.QueryRow(ctx, "SELECT transaction_type FROM gift_card_transactions WHERE gift_card_id = $1", cardID).Scan(&txType) if err != nil { t.Fatalf("failed to query transaction: %v", err) } if txType != "purchase" { t.Errorf("expected transaction type 'purchase' for first inventory top-up, got %q", txType) } } // ============================================================================= // BuyGiftCard — Additional edge cases // ============================================================================= func TestBuyGiftCard_InvalidAmount(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 7500, "recipient_type": "self", "new_card_token": "cnon:card-nonce-ok", "idempotency_key": "idempotency-invalid-amount", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestBuyGiftCard_InvalidRecipientType(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 2000, "recipient_type": "invalid", "new_card_token": "cnon:card-nonce-ok", "idempotency_key": "idempotency-invalid-recipient", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestBuyGiftCard_CardNotFound(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 2000, "recipient_type": "self", "card_id": "nonexistent-card-id", "idempotency_key": "idempotency-card-not-found", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusNotFound { t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String()) } } func TestBuyGiftCard_NoCardInfo(t *testing.T) { _, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create user: %v", err) } token := jwt.GenerateTestToken(userID, "verified_email") reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 2000, "recipient_type": "self", "idempotency_key": "idempotency-no-card", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestBuyGiftCard_Unauthenticated(t *testing.T) { _, _ = testutils.SetupTestTx(t) reqBody, _ := json.Marshal(map[string]interface{}{ "amount": 2000, "recipient_type": "self", "new_card_token": "cnon:card-nonce-ok", }) req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r := chi.NewRouter() r.Post("/api/user/giftcards/buy", BuyGiftCard) r.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String()) } } // ============================================================================= // TestGetUserGiftCardBalanceAdmin_AuditLog verifies admin balance checks // are recorded in the admin_audit_log table. func TestGetUserGiftCardBalanceAdmin_AuditLog(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) } // Give the user a balance _, err = tx.Exec(ctx, `INSERT INTO user_giftcard_balances (user_id, balance) VALUES ($1, 42.50)`, userID) if err != nil { t.Fatalf("failed to insert user balance: %v", err) } token := jwt.GenerateTestToken(adminID, "admin") // Set up request with chi route context for URL param extraction req := httptest.NewRequest("GET", "/"+userID+"/giftcard-balance", nil) req.Header.Set("Authorization", "Bearer "+token) req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", userID) ctxWithRoute := context.WithValue(req.Context(), chi.RouteCtxKey, rctx) req = req.WithContext(ctxWithRoute) // Add user context (from token) info := extractUserFromTestJWT(token) if info != nil { reqCtx := context.WithValue(req.Context(), mw.UserIDKey, info.userID) reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, info.role) req = req.WithContext(reqCtx) } w := httptest.NewRecorder() GetUserGiftCardBalanceAdmin(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d. Body: %s", w.Code, w.Body.String()) } var resp map[string]float64 if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } if resp["balance"] != 42.50 { t.Errorf("expected balance 42.50, got %.2f", resp["balance"]) } // Verify audit log entry was created var logCount int err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM admin_audit_log WHERE admin_id = $1 AND target_user_id = $2 AND action_type = 'balance_check'`, adminID, userID).Scan(&logCount) if err != nil { t.Fatalf("failed to query admin_audit_log: %v", err) } if logCount != 1 { t.Errorf("expected 1 audit log entry, got %d", logCount) } } // ============================================================================= // TransferGiftCard — Validation gap tests // ============================================================================= func TestTransferGiftCard_InvalidFromCardID(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": "aaaaaaaaaaaa", "amount": 10.00, }) req := httptest.NewRequest("POST", "/api/admin/gift-cards/$$$/transfer", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_InvalidToCardID(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": "$$$", "amount": 10.00, }) req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_ZeroAmount(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") reqBody, _ := json.Marshal(map[string]interface{}{ "to_card_id": "aaaaaaaaaaaa", "amount": 0, }) req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } } func TestTransferGiftCard_JSONDecodeError(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) adminID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create admin: %v", err) } _, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) token := jwt.GenerateTestToken(adminID, "admin") req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer([]byte(`{invalid}`))) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx))) 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.StatusBadRequest { t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String()) } }