//go:build test && dev // +build test,dev package payments import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "crussell/db" "crussell/mw" "crussell/testutils/fixtures" "crussell/testutils/jwt" "github.com/go-chi/chi/v5" ) func TestCreateTillSale_OnTheHouse(t *testing.T) { resetTestData(t) ctx := context.Background() adminID, err := fixtures.CreateTestAdminUser(db.DB) 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") 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 = db.DB.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 = db.DB.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) { resetTestData(t) ctx := context.Background() adminID, err := fixtures.CreateTestAdminUser(db.DB) 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") 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") 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 = db.DB.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) { resetTestData(t) ctx := context.Background() adminID, err := fixtures.CreateTestAdminUser(db.DB) 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") 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 = db.DB.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 = db.DB.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) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) 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") 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) { resetTestData(t) ctx := context.Background() adminID, err := fixtures.CreateTestAdminUser(db.DB) 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(db.DB) if err != nil { t.Fatalf("failed to create redeemer user: %v", err) } // Insert a gift card that is already redeemed var cardID string err = db.DB.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") 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()) } }