# Ashby Bowl lane-booking audit findings

## Critical / high-risk flaws fixed

1. **Public booking JavaScript recursion** - the old availability bootstrap called itself after each fetch, causing repeated requests and event-handler registration. Replaced with a single state-driven flow.
2. **Hold loss during checkout navigation** - the old browser flow could clear its own pending lane while moving into checkout. The new flow distinguishes normal page exit from deliberate checkout continuation.
3. **Non-atomic lane allocation** - public and staff paths previously checked overlap without serializing lane allocation. All booking writers now lock the lane set before conflict checks and writes.
4. **Client-controlled discount amount** - the old payment endpoint accepted a posted monetary discount. The new endpoint accepts only a code and recomputes the discount from MySQL.
5. **Insufficient Stripe confirmation checks** - the old success path could mark a booking confirmed without requiring a paid Checkout Session and exact paid amount. Confirmation now verifies paid state, currency, booking/session binding, metadata and amount.
6. **No browser-independent payment confirmation** - relying only on the success return could leave paid bookings pending if the customer closed the browser. Added a signed Stripe webhook with an idempotent success-page fallback.
7. **Hold/Stripe lifetime mismatch** - a database lane hold could expire while a Stripe Checkout page was still payable. Stripe Checkout expiry and DB hold timing are now coordinated and stale Stripe Sessions are reconciled before release.
8. **Created-but-unbound Stripe Session edge case** - if Stripe creation succeeded but saving the Session ID failed, an unknown open payment page could remain. Known unbound Sessions are now explicitly expired before another attempt.
9. **Paid booking resurrection risk** - delayed/repeated payment callbacks could conflict with staff cancellation. Cancelled/refunded bookings are never silently restored by confirmation logic.
10. **Destructive booking deletion** - deleting paid bookings destroyed audit/payment linkage. The endpoint now performs soft cancellation and preserves refund/audit references.
11. **Hard-coded production credentials** - database/Stripe/SMTP secrets were present in source. Replacement configuration loads private/environment secrets; old credentials should be rotated.

## Integrity and operational flaws fixed

12. Pending holds are stable rows rather than delete/recreate records while customers change options.
13. Package/item IDs, event membership, min/max people, duration and event end are validated server-side.
14. Public option cards are filtered to packages that fit and currently have a lane free for their full duration.
15. The final price is snapshotted server-side before Stripe and checked again against Stripe's paid amount.
16. Stripe Checkout creation uses idempotency keys to protect repeat submits/network retries.
17. Open Stripe Checkout is explicitly expired on safe cancellation before the lane is released.
18. Stripe-backed stale holds are not blindly deleted by calendar refreshes.
19. Confirmation email delivery is separated from payment confirmation, so SMTP failure does not roll back the booking.
20. Separate customer/staff email flags prevent successful recipient delivery being duplicated while allowing failed delivery to retry.
21. Booking API write operations use CSRF/session ownership checks; payment/hold creation is rate-limited.
22. Pre-payment heartbeat lifetime is capped so abandoned/bot tabs cannot refresh a lane forever.
23. Event duplication and option creation preserve `pricing_mode` rather than silently reverting pricing behaviour.
24. Discount and option admin mutation forms are CSRF-protected and validated.
25. Database exceptions are logged server-side instead of exposing connection details to visitors.
26. Webhook and CLI migration paths no longer start unnecessary browser sessions.

## Preserved business rule requiring review

The existing code treats `per_person` as both per-person pricing and per-person duration multiplication. This package preserves that behaviour to avoid silently changing live products. Confirm that this is intentional before production go-live.

## External validation still required

Static and pure-function tests can be run without production access, but final go-live still requires a staging/test-mode pass against the real MySQL schema, Stripe account/webhook, SMTP service, PHP version and web-server routes.
