Fix payment review round: till integrity, HTTP client tests, concurrency tests, card-selection consolidation
Addresses the payment review (all 10 blocking + 2 minor findings): Till money-integrity (CreateTillSale): - Add pg_advisory_lock on the idempotency key (concurrent same-key double-funding race) - Guard amount on pending-reuse retry (mirrors tip/gift-card guards) - Explicitly complete the row for cash/on_the_house pending-reuse - Reject method-switch on a live card-machine checkout (double-charge guard) - 3 regression tests (amount-mismatch, cash-completes-row, method-switch) BookingFlow: - Fetch saved cards at the deposit step (was dead code) - Charge the server-computed deposit_amount, not the client estimate HTTP client tests (was untested): doJSON error parsing, refund sentinel classification, payment/refund/card wire shapes, checkout polling states, list-refunds pagination + 20-page guard, sha256 card idempotency key Concurrency regression tests: real two-goroutine races for BuyGiftCard, tip, and booking-payment locks asserting exactly-one record each Frontend: - Fix CRIT-1: zero-saved-card users blocked (all flows now handle it) - Consolidate tip/deposit/Buy-Gift-Card card UI onto CardSelection - Explicit save-card consent checkbox (was silent/inconsistent) - Fix stale saved-card field names in BookingFlow (last4 -> last_4) - Unique instance ids (crypto.randomUUID) in CardSelection/SquareCardInput - UserPaymentModal: keep card form mounted on error + Try Again button Health/docs: /api/health reports square state (mock/ok, was not_implemented), close P1 backlog, correct stale webhook and env-var claims
This commit is contained in:
@@ -279,7 +279,10 @@ type sqDisableCardResponse struct {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func createPaymentHTTP(ctx context.Context, req CreatePaymentReq) (*PaymentResult, error) {
|
||||
hc := newHTTPClient()
|
||||
return createPaymentHTTPWithClient(ctx, req, newHTTPClient())
|
||||
}
|
||||
|
||||
func createPaymentHTTPWithClient(ctx context.Context, req CreatePaymentReq, hc *httpClient) (*PaymentResult, error) {
|
||||
body := sqCreatePaymentRequest{
|
||||
SourceID: req.SourceID,
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
@@ -327,7 +330,10 @@ func createCheckoutHTTPWithClient(ctx context.Context, req CreateCheckoutReq, hc
|
||||
}
|
||||
|
||||
func getCheckoutHTTP(ctx context.Context, checkoutID string) (*PaymentResult, error) {
|
||||
hc := newHTTPClient()
|
||||
return getCheckoutHTTPWithClient(ctx, checkoutID, newHTTPClient())
|
||||
}
|
||||
|
||||
func getCheckoutHTTPWithClient(ctx context.Context, checkoutID string, hc *httpClient) (*PaymentResult, error) {
|
||||
var tcResp sqTerminalCheckoutResponse
|
||||
if err := hc.doJSON(ctx, http.MethodGet, "/v2/terminals/checkouts/"+checkoutID, nil, &tcResp); err != nil {
|
||||
return nil, err
|
||||
@@ -374,7 +380,10 @@ var definitiveRefundCodes = map[string]bool{
|
||||
}
|
||||
|
||||
func refundPaymentHTTP(ctx context.Context, req RefundPaymentReq) (*RefundResult, error) {
|
||||
hc := newHTTPClient()
|
||||
return refundPaymentHTTPWithClient(ctx, req, newHTTPClient())
|
||||
}
|
||||
|
||||
func refundPaymentHTTPWithClient(ctx context.Context, req RefundPaymentReq, hc *httpClient) (*RefundResult, error) {
|
||||
body := sqRefundPaymentRequest{
|
||||
PaymentID: req.PaymentID,
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
@@ -396,7 +405,10 @@ func refundPaymentHTTP(ctx context.Context, req RefundPaymentReq) (*RefundResult
|
||||
}
|
||||
|
||||
func listRefundsHTTP(ctx context.Context, paymentID string, beginTime time.Time) ([]RefundResult, error) {
|
||||
hc := newHTTPClient()
|
||||
return listRefundsHTTPWithClient(ctx, paymentID, beginTime, newHTTPClient())
|
||||
}
|
||||
|
||||
func listRefundsHTTPWithClient(ctx context.Context, paymentID string, beginTime time.Time, hc *httpClient) ([]RefundResult, error) {
|
||||
base := "/v2/refunds?begin_time=" + url.QueryEscape(beginTime.UTC().Format(time.RFC3339)) + "&limit=100"
|
||||
path := base
|
||||
results := []RefundResult{}
|
||||
@@ -420,7 +432,10 @@ func listRefundsHTTP(ctx context.Context, paymentID string, beginTime time.Time)
|
||||
}
|
||||
|
||||
func createCardOnFileHTTP(ctx context.Context, userID, cardToken string) (*CardOnFile, error) {
|
||||
hc := newHTTPClient()
|
||||
return createCardOnFileHTTPWithClient(ctx, userID, cardToken, newHTTPClient())
|
||||
}
|
||||
|
||||
func createCardOnFileHTTPWithClient(ctx context.Context, userID, cardToken string, hc *httpClient) (*CardOnFile, error) {
|
||||
|
||||
// Deterministic idempotency key derived from user + card (not time-based)
|
||||
// so that retries with the same details don't create duplicate cards.
|
||||
|
||||
Reference in New Issue
Block a user