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:
2026-08-22 00:34:50 +01:00
parent 9a182db932
commit 1543160f6a
12 changed files with 605 additions and 57 deletions
@@ -558,14 +558,22 @@
newCardTokenizedForSaveCard = false;
twoFactor.setCode('');
twoFactor.reveal = false;
// The backend skips the Square charge entirely when an eligible
// campaign discount covers the whole deposit
// (`deposit_covered_by_discount` — a £0 charge is invalid at
// Square). Report it as a completed, nothing-to-pay deposit.
paymentResult = {
id: data.id,
amount: data.amount,
id: data.id ?? '',
amount: data.amount ?? 0,
card_brand: data.card_brand,
card_last4: data.card_last4,
payment_type: data.payment_type
payment_type: data.payment_type ?? 'deposit'
};
toast.success('Payment successful');
toast.success(
data.deposit_covered_by_discount
? 'Deposit covered by your discount — nothing to pay'
: 'Payment successful'
);
savedCardsStore.invalidate();
onComplete();
releaseLock();