Files
Crussell/backend/handlers/payments/validators.go
T
popertots 2459ddc919 Fix review findings: expiry bug (all 8 files), idempotency keys, card_expiry/card_cvc removal, URL encoding, BuyerEmail logging, ValidateCardInfo, saved-card test, future work doc
Backend:
- Fix refund idempotency key: clock.Now() → deterministic (pr.ID + amount)
- Fix ValidateCardInfo: enforce mutual exclusivity, handle empty strings symmetrically
- Fix paymentFromSquare brand fallback (remove dead SourceType fallback)
- Fix URL encoding: PathEscape → QueryEscape for customer_id query param
- Fix BuyerEmail: log warning on DB error instead of silent discard
- Fix idempotency key in createCardOnFileHTTP: time.Now() → deterministic hex hash
- Add BuyerEmail to CreateTipPayment Square request
- Move realBaseURL from shared file to square_dev.go (only used in dev)
- Add TestTipPayment_WithSavedCard test (card_id path coverage)
- Fix AMEX brand in mock (AMEX → AMERICAN_EXPRESS, fix test)

Frontend:
- Fix off-by-month expiry bug in ALL 8 files using year-month arithmetic
  (parseExpiryParts returns 1-indexed, SvelteDate expects 0-indexed)
  Files: tip/+page, pay-tip/[id], UserBookingModal, UserPaymentModal,
  BookingFlow, account/+page (add card + buy gift card sections)
- Remove card_expiry/card_cvc from tip request bodies (backend has no fields)

Docs:
- Mark P9 (placeholder tokens) as completed, add P11 (Square Web Payments SDK)
- Mark T13 (rune arithmetic) as completed
2026-08-22 00:34:49 +01:00

69 lines
1.8 KiB
Go

package payments
import (
"errors"
"fmt"
)
// Valid payment types
var validPaymentTypes = map[string]bool{
"deposit": true,
"full": true,
"tip": true,
"balance": true,
"partial": true,
}
// ValidateAmount checks that amount is greater than 0 and has valid precision (max 2 decimal places when in pence)
func ValidateAmount(amount int64) error {
if amount <= 0 {
return errors.New("amount must be greater than 0")
}
if amount > 1_000_000 { // £10,000 in pence
return errors.New("amount exceeds maximum (£10,000)")
}
return nil
}
// ValidatePartialAmount checks that the partial amount doesn't exceed the remaining balance
func ValidatePartialAmount(amountCents int64, remainingCents int64) error {
if amountCents > remainingCents {
return fmt.Errorf("partial amount (£%.2f) exceeds remaining balance (£%.2f)",
float64(amountCents)/100, float64(remainingCents)/100)
}
if amountCents <= 0 {
return errors.New("amount must be greater than 0")
}
return nil
}
// ValidatePaymentType checks that payment type is valid
func ValidatePaymentType(pt string) error {
if !validPaymentTypes[pt] {
return errors.New("invalid payment type")
}
return nil
}
// ValidateRefundReason checks that refund reason is not empty
func ValidateRefundReason(reason string) error {
if reason == "" {
return errors.New("refund reason is required")
}
return nil
}
// ValidateCardInfo checks that exactly one of cardID or newCardToken is provided,
// non-nil, and non-empty.
func ValidateCardInfo(cardID, newCardToken *string) error {
hasCardID := cardID != nil && *cardID != ""
hasToken := newCardToken != nil && *newCardToken != ""
if hasCardID && hasToken {
return errors.New("provide either card_id or new_card_token, not both")
}
if !hasCardID && !hasToken {
return errors.New("either card_id or new_card_token is required")
}
return nil
}