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:
@@ -23,7 +23,17 @@
|
||||
defaultPaymentType?: 'full' | 'partial' | 'deposit';
|
||||
}
|
||||
|
||||
const { booking, onClose, onComplete, canSaveCards = true, defaultPaymentType }: Props = $props();
|
||||
const {
|
||||
booking,
|
||||
onClose,
|
||||
onComplete,
|
||||
canSaveCards = false,
|
||||
defaultPaymentType
|
||||
}: Props = $props();
|
||||
|
||||
// Explicit consent: whether the new card is saved for next time. Toggled by
|
||||
// the checkbox inside CardSelection; defaults to false (opt-in).
|
||||
let saveCard = $state(false);
|
||||
|
||||
type PaymentStatus = 'idle' | 'processing' | 'polling' | 'success' | 'error';
|
||||
|
||||
@@ -394,7 +404,7 @@
|
||||
amount: amountCents,
|
||||
payment_type: paymentType,
|
||||
...(cardId ? { card_id: cardId } : {}),
|
||||
...(newCardToken ? { new_card_token: newCardToken, save_card: canSaveCards } : {}),
|
||||
...(newCardToken ? { new_card_token: newCardToken, save_card: saveCard } : {}),
|
||||
idempotency_key: payIdempotencyKey
|
||||
})
|
||||
});
|
||||
@@ -687,8 +697,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Selection (only when idle) -->
|
||||
{#if status === 'idle' && authStore.isAuthenticated}
|
||||
<!-- Card Selection (mounted while idle OR error so a declined card
|
||||
can be retried/swapped without closing the modal) -->
|
||||
{#if (status === 'idle' || status === 'error') && authStore.isAuthenticated}
|
||||
{#if paymentMethodsLoading}
|
||||
<div class="py-2 text-center text-sm text-gray-500">Loading payment methods...</div>
|
||||
{:else}
|
||||
@@ -697,6 +708,7 @@
|
||||
cards={paymentMethods}
|
||||
{canSaveCards}
|
||||
bind:selectedCardId
|
||||
bind:saveCard
|
||||
onValidityChange={(v) => (cardSelectionValid = v)}
|
||||
/>
|
||||
{/if}
|
||||
@@ -849,6 +861,17 @@
|
||||
{#if status === 'error' && error}
|
||||
<div class="rounded-md border border-red-200 bg-red-50 p-3">
|
||||
<p class="text-sm text-red-800">{error}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-3 w-full"
|
||||
onclick={() => {
|
||||
status = 'idle';
|
||||
error = null;
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user