883 lines
26 KiB
Go
883 lines
26 KiB
Go
//go:build test && dev
|
|
|
|
package payments
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"crussell/db"
|
|
"crussell/internal/square"
|
|
"crussell/mw"
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
"crussell/testutils/jwt"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCreateTillSale_OnTheHouse(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
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 TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if resp.Status != "completed" {
|
|
t.Errorf("expected status 'completed', got '%s'", resp.Status)
|
|
}
|
|
if resp.PaymentMethod != "on_the_house" {
|
|
t.Errorf("expected payment method 'on_the_house', got '%s'", resp.PaymentMethod)
|
|
}
|
|
if resp.TotalAmount != 50.00 {
|
|
t.Errorf("expected total amount 50.00, got %.2f", resp.TotalAmount)
|
|
}
|
|
if resp.ItemType != "gift_card" {
|
|
t.Errorf("expected item type 'gift_card', got '%s'", resp.ItemType)
|
|
}
|
|
if resp.ItemID == nil || *resp.ItemID == "" {
|
|
t.Error("expected item_id to be set (gift card ID)")
|
|
}
|
|
|
|
// Verify till_sale was created in DB
|
|
var saleCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM till_sales WHERE id = $1", resp.ID).Scan(&saleCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query till_sales: %v", err)
|
|
}
|
|
if saleCount != 1 {
|
|
t.Errorf("expected 1 till_sale, got %d", saleCount)
|
|
}
|
|
|
|
// Verify gift card was created
|
|
var gcCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM gift_cards WHERE id = $1", *resp.ItemID).Scan(&gcCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query gift_cards: %v", err)
|
|
}
|
|
if gcCount != 1 {
|
|
t.Errorf("expected 1 gift_card, got %d", gcCount)
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_Idempotency(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
idempotencyKey := "test-idempotency-key-001"
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 25.00,
|
|
PaymentMethod: "on_the_house",
|
|
IdempotencyKey: idempotencyKey,
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
// First request
|
|
req1 := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req1.Header.Set("Authorization", "Bearer "+adminToken)
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
|
|
req1 = req1.WithContext(db.ContextWithTx(req1.Context(), tx.(pgx.Tx)))
|
|
|
|
w1 := httptest.NewRecorder()
|
|
r := chi.NewRouter()
|
|
r.Use(mw.RequireAuth)
|
|
r.Post("/api/admin/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w1, req1)
|
|
|
|
if w1.Code != http.StatusCreated {
|
|
t.Errorf("first request: expected status 201, got %d. body: %s", w1.Code, w1.Body.String())
|
|
}
|
|
|
|
var resp1 TillSaleResponse
|
|
if err := json.NewDecoder(w1.Body).Decode(&resp1); err != nil {
|
|
t.Fatalf("failed to decode first response: %v", err)
|
|
}
|
|
|
|
if resp1.ID == "" {
|
|
t.Fatal("expected first till_sale ID to be set")
|
|
}
|
|
|
|
// Second request with same idempotency key
|
|
req2 := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req2.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r2.ServeHTTP(w2, req2)
|
|
|
|
// Idempotent response returns 200 (the handler does not set WriteHeader in the idempotency path)
|
|
if w2.Code != http.StatusOK && w2.Code != http.StatusCreated {
|
|
t.Errorf("second request: expected status 200 or 201, got %d. body: %s", w2.Code, w2.Body.String())
|
|
}
|
|
|
|
var resp2 TillSaleResponse
|
|
if err := json.NewDecoder(w2.Body).Decode(&resp2); err != nil {
|
|
t.Fatalf("failed to decode second response: %v", err)
|
|
}
|
|
|
|
if resp1.ID != resp2.ID {
|
|
t.Errorf("expected same till_sale ID for idempotent request, got %s and %s", resp1.ID, resp2.ID)
|
|
}
|
|
|
|
// Verify only one till_sale exists
|
|
var saleCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM till_sales").Scan(&saleCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query till_sales: %v", err)
|
|
}
|
|
if saleCount != 1 {
|
|
t.Errorf("expected 1 till_sale (idempotent), got %d", saleCount)
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_CreatesGiftCardTransaction(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
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 TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if resp.ItemID == nil || *resp.ItemID == "" {
|
|
t.Fatal("expected item_id to be set")
|
|
}
|
|
|
|
// Verify gift_card_transactions was created
|
|
var txCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM gift_card_transactions WHERE gift_card_id = $1 AND transaction_type = 'purchase'", *resp.ItemID).Scan(&txCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query gift_card_transactions: %v", err)
|
|
}
|
|
if txCount != 1 {
|
|
t.Errorf("expected 1 gift_card_transaction with type 'purchase', got %d", txCount)
|
|
}
|
|
|
|
// Verify the transaction has reference_type = 'till_sale' and reference_id is set
|
|
var refCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM gift_card_transactions WHERE gift_card_id = $1 AND reference_type = 'till_sale' AND reference_id IS NOT NULL", *resp.ItemID).Scan(&refCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query gift_card_transactions with reference: %v", err)
|
|
}
|
|
if refCount != 1 {
|
|
t.Errorf("expected 1 gift_card_transaction with reference_type 'till_sale' and reference_id set, got %d", refCount)
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_InvalidPaymentMethod(t *testing.T) {
|
|
t.Parallel()
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "invalid_method",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_TopupOnRedeemedCard(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
// Create a test user to act as the redeemer
|
|
redeemerID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create redeemer user: %v", err)
|
|
}
|
|
|
|
// Insert a gift card that is already redeemed
|
|
var cardID string
|
|
err = tx.QueryRow(ctx, `
|
|
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_by, redeemed_at)
|
|
VALUES (50.00, 0.00, $1, $2, NOW())
|
|
RETURNING id
|
|
`, adminID, redeemerID).Scan(&cardID)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert redeemed gift card: %v", err)
|
|
}
|
|
|
|
// Try to topup the redeemed card
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "topup",
|
|
Amount: 25.00,
|
|
PaymentMethod: "on_the_house",
|
|
GiftCardID: &cardID,
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_SavedCard_TransactionFailure_SkipsSquare(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "sq_test_card_id", "VISA", "1234")
|
|
if err != nil {
|
|
t.Fatalf("failed to create saved card: %v", err)
|
|
}
|
|
|
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "saved_card",
|
|
UserSavedCardID: &cardID,
|
|
UserID: &userID,
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/admin/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected status 500, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var completedCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM till_sales WHERE status = 'completed'").Scan(&completedCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query till_sales: %v", err)
|
|
}
|
|
if completedCount != 0 {
|
|
t.Errorf("expected 0 completed till_sales, got %d", completedCount)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// GetTillCheckoutStatus — GET /api/admin/till/checkout/{checkout_id}/status
|
|
// =============================================================================
|
|
|
|
func TestGetTillCheckoutStatus_NotFound(t *testing.T) {
|
|
_, _ = testutils.SetupTestTx(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/admin/till/checkout/nonexistent/status", nil)
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("checkout_id", "nonexistent")
|
|
reqCtx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
|
|
req = req.WithContext(reqCtx)
|
|
|
|
w := httptest.NewRecorder()
|
|
GetTillCheckoutStatus(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGetTillCheckoutStatus_Pending(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
// Hold the mock checkout so it stays PENDING for testing
|
|
if mc, ok := SquareClient.(*square.MockClient); ok {
|
|
mc.HoldCheckouts = true
|
|
t.Cleanup(func() { mc.HoldCheckouts = false })
|
|
}
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
// Create a till sale with card_machine payment to generate a Square checkout.
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "card_machine",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var createResp TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&createResp); err != nil {
|
|
t.Fatalf("failed to decode create response: %v", err)
|
|
}
|
|
|
|
if createResp.CheckoutID == nil || *createResp.CheckoutID == "" {
|
|
t.Fatal("expected checkout_id to be set for card_machine payment")
|
|
}
|
|
|
|
// Now call GetTillCheckoutStatus with the checkout_id.
|
|
statusReq := httptest.NewRequest("GET", "/api/admin/till/checkout/"+*createResp.CheckoutID+"/status", nil)
|
|
statusRCtx := chi.NewRouteContext()
|
|
statusRCtx.URLParams.Add("checkout_id", *createResp.CheckoutID)
|
|
statusReqCtx := context.WithValue(statusReq.Context(), chi.RouteCtxKey, statusRCtx)
|
|
statusReqCtx = db.ContextWithTx(statusReqCtx, tx.(pgx.Tx))
|
|
statusReq = statusReq.WithContext(statusReqCtx)
|
|
|
|
wStatus := httptest.NewRecorder()
|
|
GetTillCheckoutStatus(wStatus, statusReq)
|
|
|
|
if wStatus.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d. body: %s", wStatus.Code, wStatus.Body.String())
|
|
}
|
|
|
|
var statusResp PaymentStatusResponse
|
|
if err := json.NewDecoder(wStatus.Body).Decode(&statusResp); err != nil {
|
|
t.Fatalf("failed to parse status response: %v", err)
|
|
}
|
|
if statusResp.Status != "PENDING" {
|
|
t.Errorf("expected status PENDING, got %s", statusResp.Status)
|
|
}
|
|
}
|
|
|
|
func TestGetTillCheckoutStatus_Completed(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
// Create a till sale with card_machine payment to generate a Square checkout.
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "card_machine",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var createResp TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&createResp); err != nil {
|
|
t.Fatalf("failed to decode create response: %v", err)
|
|
}
|
|
|
|
if createResp.CheckoutID == nil || *createResp.CheckoutID == "" {
|
|
t.Fatal("expected checkout_id to be set for card_machine payment")
|
|
}
|
|
|
|
// Wait for the mock goroutine to complete the checkout with polling.
|
|
require.Eventually(t, func() bool {
|
|
_, err := SquareClient.GetCheckout(context.Background(), *createResp.CheckoutID)
|
|
return err == nil
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
// Now call GetTillCheckoutStatus — should return COMPLETED.
|
|
statusReq := httptest.NewRequest("GET", "/api/admin/till/checkout/"+*createResp.CheckoutID+"/status", nil)
|
|
statusRCtx := chi.NewRouteContext()
|
|
statusRCtx.URLParams.Add("checkout_id", *createResp.CheckoutID)
|
|
statusReqCtx := context.WithValue(statusReq.Context(), chi.RouteCtxKey, statusRCtx)
|
|
statusReqCtx = db.ContextWithTx(statusReqCtx, tx.(pgx.Tx))
|
|
statusReq = statusReq.WithContext(statusReqCtx)
|
|
|
|
wStatus := httptest.NewRecorder()
|
|
GetTillCheckoutStatus(wStatus, statusReq)
|
|
|
|
if wStatus.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d. body: %s", wStatus.Code, wStatus.Body.String())
|
|
}
|
|
|
|
var statusResp PaymentStatusResponse
|
|
if err := json.NewDecoder(wStatus.Body).Decode(&statusResp); err != nil {
|
|
t.Fatalf("failed to parse status response: %v", err)
|
|
}
|
|
if statusResp.Status != "COMPLETED" {
|
|
t.Errorf("expected status COMPLETED, got %s", statusResp.Status)
|
|
}
|
|
if statusResp.PaymentID == "" {
|
|
t.Error("expected payment_id to be set")
|
|
}
|
|
}
|
|
|
|
func TestGetTillCheckoutStatus_AlreadyCompleted(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
// Create a till sale with on_the_house so it's immediately completed.
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 25.00,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var createResp TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&createResp); err != nil {
|
|
t.Fatalf("failed to decode create response: %v", err)
|
|
}
|
|
if createResp.ID == "" {
|
|
t.Fatal("expected till sale ID")
|
|
}
|
|
|
|
// on_the_house doesn't create a Square checkout, so GetTillCheckoutStatus
|
|
// with a non-existent checkout_id should return 404.
|
|
statusReq := httptest.NewRequest("GET", "/api/admin/till/checkout/nonexistent/status", nil)
|
|
statusRCtx := chi.NewRouteContext()
|
|
statusRCtx.URLParams.Add("checkout_id", "nonexistent")
|
|
statusReqCtx := context.WithValue(statusReq.Context(), chi.RouteCtxKey, statusRCtx)
|
|
statusReq = statusReq.WithContext(statusReqCtx)
|
|
|
|
wStatus := httptest.NewRecorder()
|
|
GetTillCheckoutStatus(wStatus, statusReq)
|
|
|
|
if wStatus.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d. body: %s", wStatus.Code, wStatus.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGetTillCheckoutStatus_EmptyCheckoutID(t *testing.T) {
|
|
_, _ = testutils.SetupTestTx(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/admin/till/checkout//status", nil)
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("checkout_id", "")
|
|
reqCtx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
|
|
req = req.WithContext(reqCtx)
|
|
|
|
w := httptest.NewRecorder()
|
|
GetTillCheckoutStatus(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CreateTillSale — Validation gap tests
|
|
// =============================================================================
|
|
|
|
func TestCreateTillSale_InvalidItemType(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "booking",
|
|
Action: "create",
|
|
Amount: 1000,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_InvalidAction(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "delete",
|
|
Amount: 1000,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_TopupMissingGiftCardID(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "topup",
|
|
Amount: 1000,
|
|
PaymentMethod: "on_the_house",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_SavedCardNoUserSavedCardID(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 1000,
|
|
PaymentMethod: "saved_card",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_OnlineSquareNoCardNumber(t *testing.T) {
|
|
_, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 1000,
|
|
PaymentMethod: "online_square",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestCreateTillSale_CreateCash(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
adminID, err := fixtures.CreateTestAdminUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create admin user: %v", err)
|
|
}
|
|
|
|
adminToken := jwt.GenerateTestToken(adminID, "admin")
|
|
|
|
reqBody := TillSaleRequest{
|
|
ItemType: "gift_card",
|
|
Action: "create",
|
|
Amount: 50.00,
|
|
PaymentMethod: "cash",
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
req := httptest.NewRequest("POST", "/api/admin/till/sale", bytes.NewReader(bodyBytes))
|
|
req.Header.Set("Authorization", "Bearer "+adminToken)
|
|
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/till/sale", CreateTillSale)
|
|
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 TillSaleResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if resp.Status != "completed" {
|
|
t.Errorf("expected status 'completed', got '%s'", resp.Status)
|
|
}
|
|
if resp.PaymentMethod != "cash" {
|
|
t.Errorf("expected payment method 'cash', got '%s'", resp.PaymentMethod)
|
|
}
|
|
if resp.ItemType != "gift_card" {
|
|
t.Errorf("expected item type 'gift_card', got '%s'", resp.ItemType)
|
|
}
|
|
|
|
var saleCount int
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM till_sales WHERE id = $1", resp.ID).Scan(&saleCount)
|
|
if err != nil {
|
|
t.Errorf("failed to query till_sales: %v", err)
|
|
}
|
|
if saleCount != 1 {
|
|
t.Errorf("expected 1 till_sale, got %d", saleCount)
|
|
}
|
|
}
|