feat: Square payment integration, booking flow redesign, and timezone/weekday fixes

- Add Square payment integration (mock + handlers + UI): terminal/online payments,
  refunds, tips, saved cards, webhooks. Build-tagged dev/prod clients.
- Redesign booking flow: Step 4 conditional (deposit only), Step 5 confirmation
  screen with booking ID, auto-submit on transition.
- Redesign schedule modal: 2x3 button grid with Pay Deposit/Pay Early logic.
- Add deposit warning banner at Step 1 for users with outstanding deposits.
- Fix weekday conversion bug: Go 0=Sunday vs DB 0=Monday mismatch in 6 locations.
- Fix timezone bug: UTC vs London time in closing hours validation.
- Fix frontend error parsing: plain text backend errors now displayed correctly.
- Fix crypto.randomUUID fallback for environments without Web Crypto.
- Add 7 new regression tests: closing hours, advance check, active booking limit,
  weekday conversion, UTC/London, deposit snapshot, exceptional hours.
- Fix 3 flaky tests: dynamic dates instead of fixed, no-show timing.
This commit is contained in:
2026-05-23 11:29:34 +01:00
parent bcd5ed2bd9
commit 2e1ab9d745
31 changed files with 6155 additions and 229 deletions
@@ -8,6 +8,7 @@
import * as Textarea from '$lib/components/ui/textarea';
import * as Label from '$lib/components/ui/label';
import DatePicker from '$lib/components/booking/DatePicker.svelte';
import PaymentModal from '$lib/components/payments/PaymentModal.svelte';
import type { Booking, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking';
import { extractBookedSlots, getLunchProtectionForSlots } from '$lib/lunchProtection';
@@ -94,6 +95,25 @@
.reduce((sum, p) => sum + p.amount, 0) || 0
);
let depositOutstanding = $derived(
selectedBooking?.deposit_required && !selectedBooking?.deposit_paid
);
let canPayEarly = $derived(
selectedBooking &&
!depositOutstanding &&
totalPaid < selectedBooking.total_amount &&
['confirmed', 'pending'].includes(selectedBooking.status)
);
let showPaymentModal = $state(false);
function handlePaymentComplete() {
toast.success('Payment completed');
showPaymentModal = false;
fetchBookingDetails();
}
let isRescheduleValid = $derived(
rescheduleDate && rescheduleTime && rescheduleTime.length >= 4
);
@@ -847,12 +867,37 @@
Add to Calendar
</Button>
{/if}
<Button size="sm" class="flex-1" onclick={() => (open = false)}>Close</Button>
{#if depositOutstanding}
<Button
size="sm"
class="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
onclick={() => (showPaymentModal = true)}
>
Pay Deposit
</Button>
{:else if canPayEarly}
<Button
size="sm"
class="flex-1 bg-emerald-600 hover:bg-emerald-700 text-white"
onclick={() => (showPaymentModal = true)}
>
Pay Early
</Button>
{/if}
</div>
<Button size="sm" class="w-full" variant="ghost" onclick={() => (open = false)}>Close</Button>
</div>
</Modal.Content>
</Modal.Root>
{#if showPaymentModal && selectedBooking}
<PaymentModal
booking={selectedBooking}
onClose={() => (showPaymentModal = false)}
onComplete={handlePaymentComplete}
/>
{/if}
<Modal.Root open={showCancelConfirm} onOpenChange={(v) => (showCancelConfirm = v)}>
<Modal.Content class="max-w-sm">
<Modal.Header>