# Stripe checkout reservations

A diner starts an online payment only after every session item, including set children, is served. Counter payments can still precede kitchen completion. Adding another order keeps the same table session and kitchen work.

Each online checkout stores its amount, currency, participant, account owner and original item selection before contacting Stripe. The selected quantities are reserved. Another payer can pay other quantities, but cannot reserve or pay the same ones. Split payments retain the existing independent rounding rules.

The Stripe amount and original selection are immutable. Retrying the same checkout reuses its Stripe idempotency key. A changed selection requires cancellation and a new checkout. Network calls never hold the session database lock. The API checks serving state again after Stripe responds and cancels the unpaid checkout if new unserved items appeared during that call.

`sessionStripeCheckoutItems` follows fulfillment row splits without changing the commercial selection or amount in `selectionSnapshot`. Item state changes must first redistribute settled payment quantities and then call `SessionCheckoutGuard::redistributeItem()` under the shared session lock.

## Cancellation and recovery

Diners cancel through `dine-in-app/stripe/cancel-intent.php`. The request can omit `paymentIntentId` to recover a lost intent-creation response. In that case the server resolves the existing idempotent attempt before canceling it. A request with no active owned attempt succeeds without creating a payment.

Waiters can cancel an abandoned checkout through `waiter-app/set/cancel-pending-payment.php`, using its `sessionId` and `checkoutId`. Vendor authentication scopes this action. The receipt exposes only safe pending-checkout metadata, not client secrets or another diner's permanent account details.

Reservations are released only after confirmed provider cancellation or durable capture recording. A timeout retains the reservation until a retry confirms the outcome. Processing, successful, and authorized-for-capture payments cannot be canceled through these endpoints. The signed `payment_intent.canceled` webhook also releases confirmed cancellations. These paths do not issue refunds or change recorded captured spend.

## Successful captures

Restaurants can share Stripe credentials and a webhook token. Webhook routing selects the signing credentials; the persisted payment intent selects the payment's vendor. Successful and canceled callbacks accept another vendor's intent only when both vendors share the same nonempty Stripe account key. Settlement, customer spend, finalization and notifications use the payment owner, never the first vendor matching the shared webhook token or a vendor supplied in event metadata.

`stripePaymentSettlements` provides one durable settlement record per vendor and Stripe intent, independent of active-session cleanup. Capture recording and live payment/item links commit atomically. Repeated callbacks cannot insert another payment, including after final receipt generation.

A successful capture with stale or inconsistent item coverage remains recorded as a monetary session payment. It is marked `succeeded_unmapped` for reconciliation. Captured money is never deleted because item allocation failed. A currency mismatch or a capture for an ended session remains in the durable settlement ledger without creating a misleading live bill payment.

Older `succeeded_unmapped` attempts may have no corresponding monetary session payment because earlier code removed it. Migration preserves that fact. `hasUnreconciledCapture()` blocks further collections and finalization until the manager reconciles the actual capture against the bill. It does not guess item ownership or fabricate a historical payment. Staff cancellation only cancels an unpaid checkout; it cannot reconcile or refund an already captured payment.

## Deployment

1. Leave the already applied **step 44 unchanged**.
2. Run `docs/step-45-add-stripe-checkout-reservations.sql` before deploying the new API. It creates the reservation and durable settlement tables, imports open legacy intents, and records replay protection for completed legacy callbacks.
3. Deploy the API and the waiter/dine-in changes together. Allow requests handled by the previous API workers to finish.
4. Run **step 45 again after the API cutover**. It is rerunnable: existing checkouts, amounts and settlements are preserved. This second pass catches old workers that completed during deployment and synchronizes terminal checkout state. It inserts no inferred financial payments.

Read paths ignore a legacy successful checkout's obsolete pending state only when durable replay protection exists. Unmapped captures with no recorded monetary payment continue to block collection. A local `failed` attempt remains reserved because a failed Stripe attempt can still be retried on the same intent.

An open legacy intent without its original item selection must be canceled before a new checkout starts. Reserving all current unpaid items, including after fulfillment rows split, protects against overlap; it does not establish paid item coverage. If that old intent succeeds, its actual capture can map the current unpaid bill only when it covers the current monetary remainder. An insufficient capture remains a monetary payment and leaves the difference payable at the counter.

The development schema contains the same step 45 SQL. Production and development migrations must be applied to their respective databases; integration tests reset only the dedicated test database.

## Verification

The focused PHP integration suites are `CheckoutReservationTest`, `GetIntentEndpointTest`, `WebhookEndpointTest`, `HandlePaymentIntentWithItemsTest`, and `HandlePaymentIntentFinalizesTableSessionTest`.

They cover competing diners, independent split rounding, quantities made free, lost provider responses, cancellation failure, staff vendor scope, new orders during Stripe creation, capture replay after cleanup, legacy reconciliation and migration reruns. Payment-provider calls use the local Stripe test double; they do not create real Stripe payments.
