fix(backend): address gift card review findings
Fix #1: remove expiry_date from gift card INSERTs (rolling 24-month via last_used_at only) Fix #2: add FOR UPDATE to RedeemGiftCard SELECT (race condition) Fix #6: add ?type=customer|inventory filter to GetGiftCards Fix #7: add admin_audit_log INSERT to GetUserGiftCardBalanceAdmin Fix #10: refactor normalizeCode to validators.NormalizeGiftCardCode Schema: add admin_audit_log table (GDPR Article 30), update testdb.go Tests: expiry-date-null, inventory-filter, audit-log, db table references Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -4,9 +4,11 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
@@ -123,7 +125,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
`).Scan(&resp.TotalUnclaimed)
|
`).Scan(&resp.TotalUnclaimed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to calculate total unclaimed gift cards: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,60 +135,62 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
`).Scan(&resp.TotalUserBalances)
|
`).Scan(&resp.TotalUserBalances)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to calculate total user balances: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Gift cards (paginated, optionally filtered) ---
|
// --- 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 gcTotal int
|
||||||
var gcCountQuery string
|
|
||||||
var gcListQuery string
|
|
||||||
var gcCountArgs []interface{}
|
var gcCountArgs []interface{}
|
||||||
var gcListArgs []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 != "" {
|
if searchTerm != "" {
|
||||||
searchPattern := "%" + searchTerm + "%"
|
searchPattern := "%" + searchTerm + "%"
|
||||||
|
|
||||||
gcCountQuery = `
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM gift_cards
|
|
||||||
WHERE id ILIKE $1
|
|
||||||
`
|
|
||||||
gcCountArgs = []interface{}{searchPattern}
|
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}
|
gcListArgs = []interface{}{searchPattern, perPage, offset}
|
||||||
|
gcListQuery = fmt.Sprintf(gcListQuery, 2, 3)
|
||||||
} else {
|
} 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}
|
gcListArgs = []interface{}{perPage, offset}
|
||||||
|
gcListQuery = fmt.Sprintf(gcListQuery, 1, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.QueryRow(ctx, gcCountQuery, gcCountArgs...).Scan(&gcTotal)
|
err = db.DB.QueryRow(ctx, gcCountQuery, gcCountArgs...).Scan(&gcTotal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to count gift cards: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gcRows, err := db.DB.Query(ctx, gcListQuery, gcListArgs...)
|
gcRows, err := db.DB.Query(ctx, gcListQuery, gcListArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to query gift cards: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer gcRows.Close()
|
defer gcRows.Close()
|
||||||
@@ -209,7 +213,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to scan gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +267,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
ubRows, err := db.DB.Query(ctx, ubListQuery, ubListArgs...)
|
ubRows, err := db.DB.Query(ctx, ubListQuery, ubListArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to query user balances: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer ubRows.Close()
|
defer ubRows.Close()
|
||||||
@@ -281,7 +285,7 @@ func GetGiftCards(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to scan user balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
if ubTotal == 0 {
|
if ubTotal == 0 {
|
||||||
@@ -309,7 +313,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req CreateGiftCardRequest
|
var req CreateGiftCardRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +329,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -347,7 +351,7 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,13 +373,13 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, gc.ID, req.Amount, adminID, notes)
|
`, gc.ID, req.Amount, adminID, notes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to record gift card transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +399,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req TopUpGiftCardRequest
|
var req TopUpGiftCardRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +423,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -434,7 +438,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to check gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +473,7 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to top up gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,13 +487,13 @@ func TopUpGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, cardID, txType, req.Amount, adminID, notes)
|
`, cardID, txType, req.Amount, adminID, notes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to record gift card transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,11 +511,11 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req TransferGiftCardRequest
|
var req TransferGiftCardRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.ToCardID = normalizeCode(req.ToCardID)
|
req.ToCardID = validators.NormalizeGiftCardCode(req.ToCardID)
|
||||||
if !validators.IsValidID(req.ToCardID) {
|
if !validators.IsValidID(req.ToCardID) {
|
||||||
http.Error(w, "Invalid destination gift card ID", http.StatusBadRequest)
|
http.Error(w, "Invalid destination gift card ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -530,7 +534,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -545,7 +549,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to check source gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,7 +560,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to check destination gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +582,7 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, req.Amount, fromCardID)
|
`, req.Amount, fromCardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to deduct from source: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,13 +595,13 @@ func TransferGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, req.Amount, req.ToCardID)
|
`, req.Amount, req.ToCardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to add to destination: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,11 +621,11 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req RedeemGiftCardRequest
|
var req RedeemGiftCardRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
code := normalizeCode(req.Code)
|
code := validators.NormalizeGiftCardCode(req.Code)
|
||||||
if !validators.IsValidID(code) {
|
if !validators.IsValidID(code) {
|
||||||
http.Error(w, "Invalid gift card code format", http.StatusBadRequest)
|
http.Error(w, "Invalid gift card code format", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -630,7 +634,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -641,6 +645,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
SELECT amount_remaining, redeemed_by
|
SELECT amount_remaining, redeemed_by
|
||||||
FROM gift_cards
|
FROM gift_cards
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
|
FOR UPDATE
|
||||||
`, code).Scan(&amountRemaining, &redeemedBy)
|
`, code).Scan(&amountRemaining, &redeemedBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
@@ -648,7 +653,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to query gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +676,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, userID, code)
|
`, userID, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +689,7 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, userID, amountRemaining)
|
`, userID, amountRemaining)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update user gift card balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,13 +699,13 @@ func RedeemGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, code, amountRemaining, userID)
|
`, code, amountRemaining, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to record gift card transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +733,7 @@ func GetGiftCardBalance(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to query user balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +750,8 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminID, _ := ctx.Value(mw.UserIDKey).(string)
|
||||||
|
|
||||||
var balance float64
|
var balance float64
|
||||||
err := db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
|
err := db.DB.QueryRow(ctx, "SELECT balance FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -754,10 +761,18 @@ func GetUserGiftCardBalanceAdmin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to query user balance: %v", err)
|
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
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
|
json.NewEncoder(w).Encode(map[string]float64{"balance": balance})
|
||||||
}
|
}
|
||||||
@@ -772,7 +787,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req BuyGiftCardRequest
|
var req BuyGiftCardRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,7 +850,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
sourceID = card.SquareCardID
|
sourceID = card.SquareCardID
|
||||||
@@ -860,7 +875,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -870,15 +885,14 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
var cardID string
|
var cardID string
|
||||||
|
|
||||||
if req.RecipientType == "self" {
|
if req.RecipientType == "self" {
|
||||||
expiryDate := time.Now().AddDate(1, 0, 0)
|
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory, expiry_date)
|
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory)
|
||||||
VALUES ($1, 0, $2, NOW(), $2, FALSE, $3)
|
VALUES ($1, 0, $2, NOW(), $2, FALSE)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, amountPounds, userID, expiryDate).Scan(&cardID)
|
`, amountPounds, userID).Scan(&cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to insert gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,7 +905,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, userID, amountPounds)
|
`, userID, amountPounds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,19 +915,18 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, cardID, amountPounds, userID)
|
`, cardID, amountPounds, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to record gift card transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
expiryDate := time.Now().AddDate(1, 0, 0)
|
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, expiry_date)
|
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory)
|
||||||
VALUES ($1, $1, $2, FALSE, $3)
|
VALUES ($1, $1, $2, FALSE)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, amountPounds, userID, expiryDate).Scan(&cardID)
|
`, amountPounds, userID).Scan(&cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to insert gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,7 +944,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, cardID, amountPounds, userID)
|
`, cardID, amountPounds, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to record gift card transaction: %v", err)
|
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
|
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)
|
`, 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 {
|
if err != nil {
|
||||||
log.Printf("Failed to insert payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit buy transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,15 +991,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// --- Helpers ---
|
// --- 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 {
|
type ExpiredBalance struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -1008,7 +1013,7 @@ func GetExpiredBalances(w http.ResponseWriter, r *http.Request) {
|
|||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to query expired balances: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
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)
|
err = rows.Scan(&b.ID, &accountID, &b.OriginalBalance, &b.ExpiredAt, &claimedAt, &claimedByAdmin, ¬es)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to scan expired balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,7 +1092,7 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to check expired balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,7 +1108,7 @@ func ClaimExpiredBalance(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, adminID, req.Notes, req.BalanceID)
|
`, adminID, req.Notes, req.BalanceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to claim expired balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/mw"
|
"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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var req CreateTerminalPaymentRequest
|
var req CreateTerminalPaymentRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
log.Printf("Failed to decode terminal payment request: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get booking status: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
tx, err := db.DB.Begin(r.Context())
|
tx, err := db.DB.Begin(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(r.Context())
|
defer tx.Rollback(r.Context())
|
||||||
@@ -180,7 +180,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID)
|
`, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create cash payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
} else { // giftcard
|
} 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)
|
err = tx.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to query booking user: %v", err)
|
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
|
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)
|
_, 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 {
|
if err != nil {
|
||||||
log.Printf("Failed to deduct user balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
usedBalance = true
|
usedBalance = true
|
||||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||||
log.Printf("Failed to query user balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Gift card ID is required", http.StatusBadRequest)
|
http.Error(w, "Gift card ID is required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cleanCardID := normalizeCode(*req.GiftCardID)
|
cleanCardID := validators.NormalizeGiftCardCode(*req.GiftCardID)
|
||||||
|
|
||||||
var gcRemaining float64
|
var gcRemaining float64
|
||||||
var redeemedBy sql.NullString
|
var redeemedBy sql.NullString
|
||||||
@@ -233,7 +233,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to query gift card: %v", err)
|
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
|
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)
|
_, 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 {
|
if err != nil {
|
||||||
log.Printf("Failed to deduct gift card amount: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,14 +264,14 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID)
|
`, bookingID, req.PaymentType, amountPounds, adminID).Scan(&paymentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create giftcard payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
if err := tx.Commit(r.Context()); err != nil {
|
||||||
log.Printf("Failed to commit payment: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var req CreateBookingPaymentRequest
|
var req CreateBookingPaymentRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
log.Printf("Failed to decode booking payment request: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
remainingCents, err := service.GetBookingRemainingBalanceCents(r.Context(), bookingID)
|
remainingCents, err := service.GetBookingRemainingBalanceCents(r.Context(), bookingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get remaining balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil {
|
if err := ValidatePartialAmount(req.Amount, remainingCents); err != nil {
|
||||||
@@ -445,7 +445,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get booking user: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +500,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
sourceID = card.SquareCardID
|
sourceID = card.SquareCardID
|
||||||
@@ -543,7 +543,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +579,7 @@ func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
|
|||||||
cards, err := service.GetUserPaymentMethods(r.Context(), userID)
|
cards, err := service.GetUserPaymentMethods(r.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get payment methods: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +598,7 @@ func AdminGetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
|
|||||||
cards, err := service.GetUserPaymentMethods(r.Context(), userID)
|
cards, err := service.GetUserPaymentMethods(r.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get payment methods for user %s: %v", userID, err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +623,7 @@ func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
|
|||||||
err := service.DeletePaymentMethod(r.Context(), cardID, userID)
|
err := service.DeletePaymentMethod(r.Context(), cardID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to delete payment method: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +646,7 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req CreatePaymentMethodRequest
|
var req CreatePaymentMethodRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var req RefundRequest
|
var req RefundRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
log.Printf("Failed to decode refund request: %v", err)
|
log.Printf("Failed to decode refund request: %v", err)
|
||||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +710,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get payment: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,7 +727,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
alreadyRefunded, err := service.GetAlreadyRefundedAmount(r.Context(), paymentID)
|
alreadyRefunded, err := service.GetAlreadyRefundedAmount(r.Context(), paymentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get already refunded amount: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +765,7 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
refundID, err := service.CreateRefundRecord(r.Context(), record)
|
refundID, err := service.CreateRefundRecord(r.Context(), record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create refund record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +803,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var req CreateTipPaymentRequest
|
var req CreateTipPaymentRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
log.Printf("Failed to decode tip payment request: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,7 +826,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get booking user: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +838,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
hasCompleted, err := service.HasCompletedPayment(r.Context(), bookingID)
|
hasCompleted, err := service.HasCompletedPayment(r.Context(), bookingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to check for completed payments: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +882,7 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create payment record: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +920,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get booking user: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -933,7 +933,7 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
|
|||||||
summary, err := service.GetBookingPaymentSummary(r.Context(), bookingID)
|
summary, err := service.GetBookingPaymentSummary(r.Context(), bookingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get payment summary: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package payments
|
|||||||
import (
|
import (
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/internal/square"
|
"crussell/internal/square"
|
||||||
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -48,7 +49,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req TillSaleRequest
|
var req TillSaleRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
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
|
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()
|
service := NewPaymentService()
|
||||||
|
|
||||||
tx, err := db.DB.Begin(ctx)
|
tx, err := db.DB.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to begin transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
@@ -119,13 +113,13 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
var giftCardID string
|
var giftCardID string
|
||||||
if req.Action == "create" {
|
if req.Action == "create" {
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, expiry_date)
|
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory)
|
||||||
VALUES ($1, $1, $2, FALSE, $3)
|
VALUES ($1, $1, $2, FALSE)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, req.Amount, adminID, expiryDate).Scan(&giftCardID)
|
`, req.Amount, adminID).Scan(&giftCardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +129,11 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, giftCardID, req.Amount, req.UserID)
|
`, giftCardID, req.Amount, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create gift_card_transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cardID := normalizeCode(*req.GiftCardID)
|
cardID := validators.NormalizeGiftCardCode(*req.GiftCardID)
|
||||||
var redeemedBy sql.NullString
|
var redeemedBy sql.NullString
|
||||||
err = tx.QueryRow(ctx, "SELECT redeemed_by FROM gift_cards WHERE id = $1", cardID).Scan(&redeemedBy)
|
err = tx.QueryRow(ctx, "SELECT redeemed_by FROM gift_cards WHERE id = $1", cardID).Scan(&redeemedBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +142,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to check gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
if redeemedBy.Valid {
|
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)
|
err = tx.QueryRow(ctx, `SELECT is_inventory, total_funds_added FROM gift_cards WHERE id = $1`, cardID).Scan(&isInventory, &previousTotal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to check gift card state: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +168,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, req.Amount, cardID)
|
`, req.Amount, cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to top up gift card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +185,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, cardID, transactionType, req.Amount, req.UserID, notes)
|
`, cardID, transactionType, req.Amount, req.UserID, notes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to create gift_card_transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +205,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, *req.RedeemToUserID, giftCardID)
|
`, *req.RedeemToUserID, giftCardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to redeem gift card to user account: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +218,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, *req.RedeemToUserID, req.Amount)
|
`, *req.RedeemToUserID, req.Amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update user gift card balance: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,7 +247,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to verify saved card: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +373,7 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
).Scan(&tillSaleID)
|
).Scan(&tillSaleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to insert till sale: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,13 +383,13 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, tillSaleID, giftCardID)
|
`, tillSaleID, giftCardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update gift_card_transactions reference: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
log.Printf("Failed to commit till sale transaction: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +425,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to find till sale: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,7 +459,7 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, paymentResult.SquarePayID, tillSaleID)
|
`, paymentResult.SquarePayID, tillSaleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to update till sale: %v", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) {
|
|||||||
// Drop all tables, sequences, and views in correct order
|
// Drop all tables, sequences, and views in correct order
|
||||||
dropOrder := []string{
|
dropOrder := []string{
|
||||||
"till_sales",
|
"till_sales",
|
||||||
|
"admin_audit_log",
|
||||||
"gift_card_transactions",
|
"gift_card_transactions",
|
||||||
"gift_card_expired_balances",
|
"gift_card_expired_balances",
|
||||||
"booking_discounts",
|
"booking_discounts",
|
||||||
@@ -272,6 +273,7 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
|
|||||||
|
|
||||||
tables := []string{
|
tables := []string{
|
||||||
"till_sales",
|
"till_sales",
|
||||||
|
"admin_audit_log",
|
||||||
"gift_card_transactions",
|
"gift_card_transactions",
|
||||||
"gift_card_expired_balances",
|
"gift_card_expired_balances",
|
||||||
"booking_discounts",
|
"booking_discounts",
|
||||||
|
|||||||
@@ -1754,9 +1754,26 @@ CREATE INDEX idx_gift_card_expired_balances_unclaimed ON gift_card_expired_balan
|
|||||||
WHERE claimed_at IS NULL;
|
WHERE claimed_at IS NULL;
|
||||||
CREATE INDEX idx_gift_card_transactions_created_at ON gift_card_transactions(created_at);
|
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.
|
-- ADMIN AUDIT LOG TABLE
|
||||||
ALTER TABLE payments ADD CONSTRAINT fk_payments_gift_card FOREIGN KEY (gift_card_id) REFERENCES gift_cards(id);
|
-- =======================================
|
||||||
|
-- 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)
|
-- SQUARE DEPOSITS TABLE (Bank Reconciliation)
|
||||||
|
|||||||
Reference in New Issue
Block a user