Fix review findings: BuyGiftCard concurrency lock, amount guards, NULL scan, mock dedup, docs
N1 (HIGH) — BuyGiftCard concurrent same-key retry could double-issue gift cards (2× value for 1 charge). Added pg_advisory_lock on the idempotency key (mirroring the tip pattern) acquired before the idempotency check, so concurrent same-key retries serialize and only one executes gift-card creation. N2 — Amount-equality guards in both reuse branches (CreateTipPayment and BuyGiftCard). A same-key retry with a different amount now returns 400 instead of silently mutating the pending record's books/VAT/refund caps. N3 — test coverage: - TestBuyGiftCard_RetryPending_ReattemptsCharge: pending record + same-key retry re-attempts, reuses the record (count=1), completes, and issues the gift card exactly once. - TestCreateCheckoutHTTP_DeviceOptionsWireShape: httptest.Server asserts device_id is under checkout.device_options (not top-level). Extracted createCheckoutHTTPWithClient for injectable base URL. - MockClient.CreatePayment now dedups on idempotency key (paymentByKey map), matching real Square behaviour. N4 — Corrected the savepoint comments in handlers.go and giftcards.go: the savepoint only exists in the test harness; in production db.Conn.Begin is a plain tx and the status UPDATE runs on a separate pooled connection. Commit is a harmless no-op in prod but required in tests. Bonus bug fixed: CheckIdempotencyByKey scanned NULL booking_id/gift_card_id (gift-card purchases) into plain string, failing with 'cannot scan NULL'. Now uses sql.NullString. Docs: Technical Manual.md:53 and Feature Catalog.md (2.1, 2.5) corrected — no longer claim Web Payments SDK is live; new-card entry is documented as pending P11, saved-card flow works via ccof tokens, dev mock rejects raw PANs.
This commit is contained in:
@@ -28,6 +28,7 @@ type MockClient struct {
|
||||
cards map[string]map[string]*CardOnFile
|
||||
checkouts map[string]*CheckoutResult
|
||||
payments map[string]*PaymentResult
|
||||
paymentByKey map[string]*PaymentResult
|
||||
refunds map[string]*RefundResult
|
||||
completed map[string]*PaymentResult
|
||||
HoldCheckouts bool
|
||||
@@ -73,11 +74,12 @@ func NewDevClient() SquareClient {
|
||||
}
|
||||
log.Println("[SQUARE-MOCK] Using in-memory mock client")
|
||||
return &MockClient{
|
||||
cards: make(map[string]map[string]*CardOnFile),
|
||||
checkouts: make(map[string]*CheckoutResult),
|
||||
payments: make(map[string]*PaymentResult),
|
||||
refunds: make(map[string]*RefundResult),
|
||||
completed: make(map[string]*PaymentResult),
|
||||
cards: make(map[string]map[string]*CardOnFile),
|
||||
checkouts: make(map[string]*CheckoutResult),
|
||||
payments: make(map[string]*PaymentResult),
|
||||
paymentByKey: make(map[string]*PaymentResult),
|
||||
refunds: make(map[string]*RefundResult),
|
||||
completed: make(map[string]*PaymentResult),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +108,17 @@ func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Real Square dedups on idempotency key: a retry with the same key returns
|
||||
// the original payment rather than creating a second charge. The mock
|
||||
// mirrors this so dev/testing behaves like production (also why the tip
|
||||
// retry regression test can rely on the mock).
|
||||
if req.IdempotencyKey != "" {
|
||||
if existing, ok := m.paymentByKey[req.IdempotencyKey]; ok {
|
||||
log.Printf("[SQUARE-MOCK] CreatePayment dedup hit: key=%s → id=%s", req.IdempotencyKey, existing.ID)
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
|
||||
now := clock.Now().UTC()
|
||||
|
||||
status := "COMPLETED"
|
||||
@@ -162,6 +175,9 @@ func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*
|
||||
}
|
||||
m.payments[paymentID] = result
|
||||
m.payments[result.SquarePayID] = result
|
||||
if req.IdempotencyKey != "" {
|
||||
m.paymentByKey[req.IdempotencyKey] = result
|
||||
}
|
||||
log.Printf("[SQUARE-MOCK] Payment created: id=%s, status=%s, amount=%d, fees=%d", paymentID, status, amount, fees)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user