diff --git a/backend/handlers/payments/giftcards.go b/backend/handlers/payments/giftcards.go index f8e7ba5..7ff0637 100644 --- a/backend/handlers/payments/giftcards.go +++ b/backend/handlers/payments/giftcards.go @@ -4,9 +4,11 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "log" "net/http" "strconv" + "strings" "time" "crussell/db" @@ -123,7 +125,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { `).Scan(&resp.TotalUnclaimed) if err != nil { log.Printf("Failed to calculate total unclaimed gift cards: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -133,60 +135,62 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { `).Scan(&resp.TotalUserBalances) if err != nil { log.Printf("Failed to calculate total user balances: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } // --- Gift cards (paginated, optionally filtered) --- + filterType := query.Get("type") + whereClauses := []string{} + if searchTerm != "" { + whereClauses = append(whereClauses, "id ILIKE $1") + } + if filterType == "customer" { + whereClauses = append(whereClauses, "is_inventory = FALSE") + } else if filterType == "inventory" { + whereClauses = append(whereClauses, "is_inventory = TRUE") + } + + whereSQL := "" + if len(whereClauses) > 0 { + whereSQL = "WHERE " + strings.Join(whereClauses, " AND ") + } + var gcTotal int - var gcCountQuery string - var gcListQuery string var gcCountArgs []interface{} var gcListArgs []interface{} + gcCountQuery := fmt.Sprintf(`SELECT COUNT(*) FROM gift_cards %s`, whereSQL) + gcListQuery := fmt.Sprintf(` + SELECT id, total_funds_added, amount_remaining, created_by, created_at, redeemed_at, redeemed_by, is_inventory, last_used_at + FROM gift_cards + %s + ORDER BY created_at DESC + LIMIT $%%d OFFSET $%%d + `, whereSQL) + if searchTerm != "" { searchPattern := "%" + searchTerm + "%" - - gcCountQuery = ` - SELECT COUNT(*) - FROM gift_cards - WHERE id ILIKE $1 - ` gcCountArgs = []interface{}{searchPattern} - - gcListQuery = ` - SELECT id, total_funds_added, amount_remaining, created_by, created_at, redeemed_at, redeemed_by, is_inventory, last_used_at - FROM gift_cards - WHERE id ILIKE $1 - ORDER BY created_at DESC - LIMIT $2 OFFSET $3 - ` gcListArgs = []interface{}{searchPattern, perPage, offset} + gcListQuery = fmt.Sprintf(gcListQuery, 2, 3) } else { - gcCountQuery = `SELECT COUNT(*) FROM gift_cards` - gcCountArgs = []interface{}{} - - gcListQuery = ` - SELECT id, total_funds_added, amount_remaining, created_by, created_at, redeemed_at, redeemed_by, is_inventory, last_used_at - FROM gift_cards - ORDER BY created_at DESC - LIMIT $1 OFFSET $2 - ` gcListArgs = []interface{}{perPage, offset} + gcListQuery = fmt.Sprintf(gcListQuery, 1, 2) } err = db.DB.QueryRow(ctx, gcCountQuery, gcCountArgs...).Scan(&gcTotal) if err != nil { log.Printf("Failed to count gift cards: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } gcRows, err := db.DB.Query(ctx, gcListQuery, gcListArgs...) if err != nil { log.Printf("Failed to query gift cards: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer gcRows.Close() @@ -209,7 +213,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { ) if err != nil { log.Printf("Failed to scan gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -263,7 +267,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { ubRows, err := db.DB.Query(ctx, ubListQuery, ubListArgs...) if err != nil { log.Printf("Failed to query user balances: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer ubRows.Close() @@ -281,7 +285,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) { ) if err != nil { log.Printf("Failed to scan user balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if ubTotal == 0 { @@ -309,7 +313,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) { var req CreateGiftCardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -325,7 +329,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -347,7 +351,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) { ) if err != nil { log.Printf("Failed to create gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -369,13 +373,13 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) { `, gc.ID, req.Amount, adminID, notes) if err != nil { log.Printf("Failed to record gift card transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -395,7 +399,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { var req TopUpGiftCardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -419,7 +423,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -434,7 +438,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to check gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -469,7 +473,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { ) if err != nil { log.Printf("Failed to top up gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -483,13 +487,13 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) { `, cardID, txType, req.Amount, adminID, notes) if err != nil { log.Printf("Failed to record gift card transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -507,11 +511,11 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { var req TransferGiftCardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } - req.ToCardID = normalizeCode(req.ToCardID) + req.ToCardID = validators.NormalizeGiftCardCode(req.ToCardID) if !validators.IsValidID(req.ToCardID) { http.Error(w, "Invalid destination gift card ID", http.StatusBadRequest) return @@ -530,7 +534,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -545,7 +549,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to check source gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -556,7 +560,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to check destination gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -578,7 +582,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { `, req.Amount, fromCardID) if err != nil { log.Printf("Failed to deduct from source: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -591,13 +595,13 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) { `, req.Amount, req.ToCardID) if err != nil { log.Printf("Failed to add to destination: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -617,11 +621,11 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { var req RedeemGiftCardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } - code := normalizeCode(req.Code) + code := validators.NormalizeGiftCardCode(req.Code) if !validators.IsValidID(code) { http.Error(w, "Invalid gift card code format", http.StatusBadRequest) return @@ -630,7 +634,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -641,6 +645,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { SELECT amount_remaining, redeemed_by FROM gift_cards WHERE id = $1 + FOR UPDATE `, code).Scan(&amountRemaining, &redeemedBy) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -648,7 +653,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to query gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -671,7 +676,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { `, userID, code) if err != nil { log.Printf("Failed to update gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -684,7 +689,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { `, userID, amountRemaining) if err != nil { log.Printf("Failed to update user gift card balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -694,13 +699,13 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) { `, code, amountRemaining, userID) if err != nil { log.Printf("Failed to record gift card transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -728,7 +733,7 @@ func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to query user balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -745,6 +750,8 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) { return } + adminID, _ := ctx.Value(mw.UserIDKey).(string) + var balance float64 err := db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance) if err != nil { @@ -754,10 +761,18 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to query user balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } + detailsJSON := fmt.Sprintf(`{"balance": %.2f}`, balance) + if _, err := db.DB.Exec(ctx, ` + INSERT INTO admin_audit_log (admin_id, action_type, target_user_id, details) + VALUES ($1, 'balance_check', $2, $3::jsonb) + `, adminID, userID, detailsJSON); err != nil { + log.Printf("Failed to record admin_audit_log (non-critical): %v", err) + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]float64{"balance": balance}) } @@ -772,7 +787,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { var req BuyGiftCardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -835,7 +850,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } sourceID = card.SquareCardID @@ -860,7 +875,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -870,15 +885,14 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { var cardID string if req.RecipientType == "self" { - expiryDate := time.Now().AddDate(1, 0, 0) err = tx.QueryRow(ctx, ` - INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory, expiry_date) - VALUES ($1, 0, $2, NOW(), $2, FALSE, $3) + INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory) + VALUES ($1, 0, $2, NOW(), $2, FALSE) RETURNING id - `, amountPounds, userID, expiryDate).Scan(&cardID) + `, amountPounds, userID).Scan(&cardID) if err != nil { log.Printf("Failed to insert gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -891,7 +905,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { `, userID, amountPounds) if err != nil { log.Printf("Failed to update balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -901,19 +915,18 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { `, cardID, amountPounds, userID) if err != nil { log.Printf("Failed to record gift card transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } else { - expiryDate := time.Now().AddDate(1, 0, 0) err = tx.QueryRow(ctx, ` - INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, expiry_date) - VALUES ($1, $1, $2, FALSE, $3) + INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) + VALUES ($1, $1, $2, FALSE) RETURNING id - `, amountPounds, userID, expiryDate).Scan(&cardID) + `, amountPounds, userID).Scan(&cardID) if err != nil { log.Printf("Failed to insert gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -931,7 +944,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { `, cardID, amountPounds, userID) if err != nil { log.Printf("Failed to record gift card transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } @@ -957,13 +970,13 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { `, record.PaymentType, record.PaymentMethod, record.Status, record.Amount, record.SquarePaymentID, record.IdempotencyKey, record.Fees, record.UserSavedCardID, record.CreatedBy, record.CreatedAt, record.UpdatedAt) if err != nil { log.Printf("Failed to insert payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit buy transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -978,15 +991,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) { // --- Helpers --- -func normalizeCode(code string) string { - clean := "" - for _, char := range code { - if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') { - clean += string(char) - } - } - return clean -} + type ExpiredBalance struct { ID string `json:"id"` @@ -1008,7 +1013,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) { `) if err != nil { log.Printf("Failed to query expired balances: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer rows.Close() @@ -1022,7 +1027,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) { err = rows.Scan(&b.ID, &accountID, &b.OriginalBalance, &b.ExpiredAt, &claimedAt, &claimedByAdmin, ¬es) if err != nil { log.Printf("Failed to scan expired balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -1087,7 +1092,7 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to check expired balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -1103,7 +1108,7 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) { `, adminID, req.Notes, req.BalanceID) if err != nil { log.Printf("Failed to claim expired balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } diff --git a/backend/handlers/payments/giftcards_test.go b/backend/handlers/payments/giftcards_test.go index 0260960..0be44bd 100644 --- a/backend/handlers/payments/giftcards_test.go +++ b/backend/handlers/payments/giftcards_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "crussell/db" "crussell/mw" @@ -1121,3 +1122,229 @@ func TestBuyGiftCard_Idempotency(t *testing.T) { 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) { + resetGiftCardsTestData(t) + + ctx := context.Background() + adminID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin: %v", err) + } + _, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) + + token := jwt.GenerateTestToken(adminID, "admin") + + reqBody, _ := json.Marshal(map[string]interface{}{"amount": 50.00}) + req := httptest.NewRequest("POST", "/api/admin/gift-cards", bytes.NewBuffer(reqBody)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + r := chi.NewRouter() + r.Use(mw.RequireAuth) + r.Post("/api/admin/gift-cards", CreateGiftCard) + r.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.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 = db.DB.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) { + resetGiftCardsTestData(t) + + ctx := context.Background() + adminID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin: %v", err) + } + _, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) + + token := jwt.GenerateTestToken(adminID, "admin") + + // Insert one customer card and one inventory card + _, err = db.DB.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 = db.DB.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) + + 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) + + 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) + + 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) + } +} + +// TestGetUserGiftCardBalanceAdmin_AuditLog verifies admin balance checks +// are recorded in the admin_audit_log table. +func TestGetUserGiftCardBalanceAdmin_AuditLog(t *testing.T) { + resetGiftCardsTestData(t) + + ctx := context.Background() + adminID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create admin: %v", err) + } + _, _ = db.DB.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID) + + userID, err := fixtures.CreateTestUser(db.DB) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + // Give the user a balance + _, err = db.DB.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) + + 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 = db.DB.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) + } +} diff --git a/backend/handlers/payments/handlers.go b/backend/handlers/payments/handlers.go index 462cece..18c20e4 100644 --- a/backend/handlers/payments/handlers.go +++ b/backend/handlers/payments/handlers.go @@ -106,7 +106,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { var req CreateTerminalPaymentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Failed to decode terminal payment request: %v", err) - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -129,7 +129,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get booking status: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -163,7 +163,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(r.Context()) @@ -180,7 +180,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { `, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID) if err != nil { log.Printf("Failed to create cash payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } else { // giftcard @@ -188,7 +188,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { err = tx.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&customerID) if err != nil { log.Printf("Failed to query booking user: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -205,13 +205,13 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { _, err = tx.Exec(r.Context(), "UPDATE user_giftcard_balances SET balance = balance - $1, updated_at = NOW() WHERE user_id = $2", amountPounds, customerID.String) if err != nil { log.Printf("Failed to deduct user balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } usedBalance = true } else if !errors.Is(err, pgx.ErrNoRows) { log.Printf("Failed to query user balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } @@ -222,7 +222,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { http.Error(w, "Gift card ID is required", http.StatusBadRequest) return } - cleanCardID := normalizeCode(*req.GiftCardID) + cleanCardID := validators.NormalizeGiftCardCode(*req.GiftCardID) var gcRemaining float64 var redeemedBy sql.NullString @@ -233,7 +233,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to query gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -251,7 +251,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { _, err = tx.Exec(r.Context(), "UPDATE gift_cards SET amount_remaining = amount_remaining - $1, last_used_at = NOW() WHERE id = $2", amountPounds, cleanCardID) if err != nil { log.Printf("Failed to deduct gift card amount: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } @@ -264,14 +264,14 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { `, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID) if err != nil { log.Printf("Failed to create giftcard payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } if err := tx.Commit(r.Context()); err != nil { log.Printf("Failed to commit payment: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -369,7 +369,7 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) { paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil) if err != nil { log.Printf("Failed to create payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -404,7 +404,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { var req CreateBookingPaymentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Failed to decode booking payment request: %v", err) - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -429,7 +429,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { remainingCents, err := service.GetBookingRemainingBalanceCents(r.Context(), bookingID) if err != nil { log.Printf("Failed to get remaining balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil { @@ -445,7 +445,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get booking user: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -500,7 +500,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } sourceID = card.SquareCardID @@ -543,7 +543,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil) if err != nil { log.Printf("Failed to create payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -579,7 +579,7 @@ func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) { cards, err := service.GetUserPaymentMethods(r.Context(), userID) if err != nil { log.Printf("Failed to get payment methods: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -598,7 +598,7 @@ func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) { cards, err := service.GetUserPaymentMethods(r.Context(), userID) if err != nil { log.Printf("Failed to get payment methods for user %s: %v", userID, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -623,7 +623,7 @@ func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) { err := service.DeletePaymentMethod(r.Context(), cardID, userID) if err != nil { log.Printf("Failed to delete payment method: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -646,7 +646,7 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) { var req CreatePaymentMethodRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -687,7 +687,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) { var req RefundRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Failed to decode refund request: %v", err) - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -710,7 +710,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get payment: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -727,7 +727,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) { alreadyRefunded, err := service.GetAlreadyRefundedAmount(r.Context(), paymentID) if err != nil { log.Printf("Failed to get already refunded amount: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -765,7 +765,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) { refundID, err := service.CreateRefundRecord(r.Context(), record) if err != nil { log.Printf("Failed to create refund record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -803,7 +803,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) { var req CreateTipPaymentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Failed to decode tip payment request: %v", err) - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -826,7 +826,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get booking user: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -838,7 +838,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) { hasCompleted, err := service.HasCompletedPayment(r.Context(), bookingID) if err != nil { log.Printf("Failed to check for completed payments: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -882,7 +882,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) { paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil) if err != nil { log.Printf("Failed to create payment record: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -920,7 +920,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to get booking user: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -933,7 +933,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) { summary, err := service.GetBookingPaymentSummary(r.Context(), bookingID) if err != nil { log.Printf("Failed to get payment summary: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } diff --git a/backend/handlers/payments/till.go b/backend/handlers/payments/till.go index a2a8027..ea704f1 100644 --- a/backend/handlers/payments/till.go +++ b/backend/handlers/payments/till.go @@ -3,6 +3,7 @@ package payments import ( "crussell/db" "crussell/internal/square" + "crussell/internal/validators" "crussell/mw" "database/sql" "encoding/json" @@ -48,7 +49,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { var req TillSaleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) + http.Error(w, "invalid request", http.StatusBadRequest) return } @@ -99,19 +100,12 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { } } - var expiryMonths int - err := db.DB.QueryRow(ctx, `SELECT COALESCE(gift_card_expiry_months, 12) FROM business_settings LIMIT 1`).Scan(&expiryMonths) - if err != nil { - expiryMonths = 12 - } - expiryDate := time.Now().AddDate(0, expiryMonths, 0) - service := NewPaymentService() tx, err := db.DB.Begin(ctx) if err != nil { log.Printf("Failed to begin transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } defer tx.Rollback(ctx) @@ -119,13 +113,13 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { var giftCardID string if req.Action == "create" { err = tx.QueryRow(ctx, ` - INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, expiry_date) - VALUES ($1, $1, $2, FALSE, $3) + INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) + VALUES ($1, $1, $2, FALSE) RETURNING id - `, req.Amount, adminID, expiryDate).Scan(&giftCardID) + `, req.Amount, adminID).Scan(&giftCardID) if err != nil { log.Printf("Failed to create gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -135,11 +129,11 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, giftCardID, req.Amount, req.UserID) if err != nil { log.Printf("Failed to create gift_card_transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } else { - cardID := normalizeCode(*req.GiftCardID) + cardID := validators.NormalizeGiftCardCode(*req.GiftCardID) var redeemedBy sql.NullString err = tx.QueryRow(ctx, "SELECT redeemed_by FROM gift_cards WHERE id = $1", cardID).Scan(&redeemedBy) if err != nil { @@ -148,7 +142,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to check gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if redeemedBy.Valid { @@ -161,7 +155,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { err = tx.QueryRow(ctx, `SELECT is_inventory, total_funds_added FROM gift_cards WHERE id = $1`, cardID).Scan(&isInventory, &previousTotal) if err != nil { log.Printf("Failed to check gift card state: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -174,7 +168,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, req.Amount, cardID) if err != nil { log.Printf("Failed to top up gift card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -191,7 +185,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, cardID, transactionType, req.Amount, req.UserID, notes) if err != nil { log.Printf("Failed to create gift_card_transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -211,7 +205,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, *req.RedeemToUserID, giftCardID) if err != nil { log.Printf("Failed to redeem gift card to user account: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -224,7 +218,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, *req.RedeemToUserID, req.Amount) if err != nil { log.Printf("Failed to update user gift card balance: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } @@ -253,7 +247,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to verify saved card: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } } @@ -379,7 +373,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { ).Scan(&tillSaleID) if err != nil { log.Printf("Failed to insert till sale: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -389,13 +383,13 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) { `, tillSaleID, giftCardID) if err != nil { log.Printf("Failed to update gift_card_transactions reference: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } if err := tx.Commit(ctx); err != nil { log.Printf("Failed to commit till sale transaction: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -431,7 +425,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) { return } log.Printf("Failed to find till sale: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } @@ -465,7 +459,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) { `, paymentResult.SquarePayID, tillSaleID) if err != nil { log.Printf("Failed to update till sale: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) return } diff --git a/backend/testutils/testdb/testdb.go b/backend/testutils/testdb/testdb.go index 9bf49e2..b2e1957 100644 --- a/backend/testutils/testdb/testdb.go +++ b/backend/testutils/testdb/testdb.go @@ -103,6 +103,7 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) { // Drop all tables, sequences, and views in correct order dropOrder := []string{ "till_sales", + "admin_audit_log", "gift_card_transactions", "gift_card_expired_balances", "booking_discounts", @@ -272,6 +273,7 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) { tables := []string{ "till_sales", + "admin_audit_log", "gift_card_transactions", "gift_card_expired_balances", "booking_discounts", diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index 94befb5..9c7edad 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -1754,9 +1754,26 @@ CREATE INDEX idx_gift_card_expired_balances_unclaimed ON gift_card_expired_balan WHERE claimed_at IS NULL; CREATE INDEX idx_gift_card_transactions_created_at ON gift_card_transactions(created_at); --- Foreign Key Constraints added after all dependent tables are created --- to avoid creation order dependencies during schema initialization. -ALTER TABLE payments ADD CONSTRAINT fk_payments_gift_card FOREIGN KEY (gift_card_id) REFERENCES gift_cards(id); +-- ======================================= +-- ADMIN AUDIT LOG TABLE +-- ======================================= +-- Records admin actions that access or modify user financial data. +-- Legal basis: GDPR Article 30 (records of processing) and financial audit requirements. +-- ======================================= + +CREATE TABLE admin_audit_log ( + id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('admin_audit_log'), + admin_id CHAR(12) NOT NULL REFERENCES users(id), + action_type VARCHAR(30) NOT NULL, + target_user_id CHAR(12) REFERENCES users(id), + target_gift_card_id CHAR(12) REFERENCES gift_cards(id), + details JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_admin_audit_log_admin ON admin_audit_log(admin_id); +CREATE INDEX idx_admin_audit_log_target_user ON admin_audit_log(target_user_id); +CREATE INDEX idx_admin_audit_log_created_at ON admin_audit_log(created_at); -- ======================================= -- SQUARE DEPOSITS TABLE (Bank Reconciliation)