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
This commit is contained in:
@@ -463,3 +463,10 @@ func isAllDigits(s string) bool {
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
func realBaseURL(env string) string {
|
||||
if env == "production" {
|
||||
return squareProductionURL
|
||||
}
|
||||
return squareSandboxURL
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
@@ -339,8 +340,15 @@ func refundPaymentHTTP(ctx context.Context, req RefundPaymentReq) (*RefundResult
|
||||
|
||||
func createCardOnFileHTTP(ctx context.Context, userID, cardToken string) (*CardOnFile, error) {
|
||||
hc := newHTTPClient()
|
||||
|
||||
// Deterministic idempotency key derived from user + card (not time-based)
|
||||
// so that retries with the same details don't create duplicate cards.
|
||||
ikHash := fmt.Sprintf("%x", []byte(userID+"|"+cardToken))
|
||||
if len(ikHash) > 64 {
|
||||
ikHash = ikHash[:64]
|
||||
}
|
||||
body := sqCreateCardRequest{
|
||||
IdempotencyKey: fmt.Sprintf("create-card-%d", time.Now().UnixNano()),
|
||||
IdempotencyKey: fmt.Sprintf("create-card-%s", ikHash),
|
||||
SourceID: cardToken,
|
||||
Card: sqCardPayload{
|
||||
CustomerID: userID,
|
||||
@@ -358,7 +366,7 @@ func createCardOnFileHTTP(ctx context.Context, userID, cardToken string) (*CardO
|
||||
func getCardsOnFileHTTP(ctx context.Context, userID string) ([]CardOnFile, error) {
|
||||
hc := newHTTPClient()
|
||||
var resp sqListCardsResponse
|
||||
if err := hc.doJSON(ctx, http.MethodGet, "/v2/cards?customer_id="+userID, nil, &resp); err != nil {
|
||||
if err := hc.doJSON(ctx, http.MethodGet, "/v2/cards?customer_id="+url.QueryEscape(userID), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cards := make([]CardOnFile, 0, len(resp.Cards))
|
||||
@@ -416,8 +424,11 @@ func paymentFromSquare(sq *sqPayment) *PaymentResult {
|
||||
r.ExpYear = cd.Card.ExpYear
|
||||
}
|
||||
}
|
||||
if r.CardBrand == "" {
|
||||
r.CardBrand = sq.SourceType
|
||||
if r.CardBrand == "" && sq.CardDetails != nil && sq.CardDetails.Card.ID != "" {
|
||||
r.CardBrand = sq.CardDetails.Card.CardBrand
|
||||
}
|
||||
if r.CardLast4 == "" && sq.CardDetails != nil && sq.CardDetails.Card.ID != "" {
|
||||
r.CardLast4 = sq.CardDetails.Card.Last4
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -479,10 +490,3 @@ func firstNonEmpty(vals ...string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func realBaseURL(env string) string {
|
||||
if env == "production" {
|
||||
return squareProductionURL
|
||||
}
|
||||
return squareSandboxURL
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user