fix: loop-B full-scope adversarial findings — tip-excluded detail endpoints, £0-charge guard, 24h window, 2FA single-use everywhere
Loop B full-scope red-team (money/security/dup-mod) findings: - CRITICAL: booking detail handlers (GetBookingHandler/GetAdminBookingHandler) now exclude payment_type='tip' from amount_paid — a tip before the final balance no longer undercharges the booking (bookings.go x3 sites) - HIGH: A6 deposit clamp adds a zero-guard — when the eligible discount covers the entire deposit, the flow returns deposit_covered_by_discount instead of charging £0 at Square (real Square rejects £0; the mock accepted it); square_dev CreatePayment + CreateRefund now reject Amount <= 0 (mock/prod parity) - HIGH: replayLegitimateRetryWindow restored to 22h (== stalePendingKeyedAge) so sweep-produced duplicate charges are still auto-refunded, not rescued-and-hidden - HIGH: 2FA single-use consume-at-gate applied to ALL saved-card charge gates (booking 2263, admin saved-card 960, tip 4483, till 967, gift-card purchase 1482) with re-issue-on-failed-charge on each; pending-reuse retries keep their code - MEDIUM: 2FA re-issue now fires only when the gate actually consumed a code (fresh saved-card path) — new-card failures no longer silently burn a standing code - MEDIUM: pre_start tip-exclusion consistent across admin lists + detail handlers (bookings.go) - MEDIUM: remaining-balance counts pending refunds (service.go) — capacity consistent with GetBookingPaymentInfo - Mock CreatePayment/CreateRefund reject £0 amounts (INVALID_REQUEST_ERROR) for dev/prod parity 26/26 backend packages; 80/80 frontend tests + build; env-docs 41/41.
This commit is contained in:
@@ -297,6 +297,22 @@ func (m *MockClient) CreatePayment(ctx context.Context, req CreatePaymentReq) (*
|
||||
if !isTokenLike(req.SourceID) {
|
||||
return nil, fmt.Errorf("invalid source_id: %s — use a card nonce (cnon:xxx) or card ID (ccof:xxx)", tokenPrefix(req.SourceID))
|
||||
}
|
||||
// Square's CreatePayment requires a positive amount_money — a missing or
|
||||
// zero amount is rejected (400 INVALID_REQUEST_ERROR), never treated as a
|
||||
// no-op. The mock mirrors the rejection so a caller that tries to charge
|
||||
// £0 (e.g. a deposit fully covered by a campaign discount) fails loudly in
|
||||
// dev instead of minting a completed £0 payment that real Square would
|
||||
// never accept (finding 2).
|
||||
if req.Amount <= 0 {
|
||||
return nil, &squareAPIError{
|
||||
Code: "INVALID_REQUEST_ERROR",
|
||||
Category: "INVALID_REQUEST_ERROR",
|
||||
Field: "amount_money",
|
||||
Detail: "The payment amount must be greater than zero",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
err: errors.New("square: payment amount must be positive (amount_money is required)"),
|
||||
}
|
||||
}
|
||||
// Square requires customer_id when charging a card-on-file (ccof:) token.
|
||||
// The mock enforces the same rule so dev parity catches the production bug
|
||||
// where a saved-card charge is sent without the customer's Square customer
|
||||
|
||||
Reference in New Issue
Block a user