Booking and payment
A customer can pay before the photographer accepts the session. Payment and approval are separate. The server prices the order. Stripe collects USD. The photographer decides after the payment is verified.
Publication and gallery approval continue in gallery approval. The upload step only stores private files. It does not grant access.
Sequence
Rules
- The order total is
package.price_centstimes quantity, minus a reserved reward discount. The browser amount is ignored. - Currency on the order, the item, and the Stripe checkout is
USD. - A new email creates
users.status = PENDING_SETUP. A verified payment queues one account setup email. An existing active email requires a valid access token and does not create a second user. - Checkout creation and the booking insert share an idempotency key. Reusing the key with the same body returns the original response. Reusing it with a different body is rejected.
- The webhook inserts
payment_webhook_eventswithUNIQUE(provider, event_id)inside the transaction that updates the payment, the order, and the booking. A duplicate event id rolls the transaction back and the handler returns success without a second update. UNIQUE(provider, provider_payment_id)stops two payment rows from storing the same provider charge.- The photographer can decide only when the booking is
PENDING_APPROVALand the order isPAID. - Approval sets
CONFIRMED,approved_by, andapproved_at. - Rejection of a paid booking sets
REJECTED, sets the order toREFUND_PENDING, and enqueuespayment.refundfor the captured USD amount. The provider call happens in the worker after the database transaction commits. - Cancellation of a confirmed, not yet completed, paid booking uses that same refund path and sets the booking to
CANCELLED. - A completed booking is not refunded by this workflow.
- A failed payment leaves the booking in
PENDING_PAYMENTand the payment rowFAILED. The customer retries with a new payment row on the same order. - If a webhook never arrives,
payment.reconcilereads the payment from Stripe and applies the same transaction as a webhook. The success page does not do this by trusting the query string. It may ask the API to reconcile, and the API trusts only the Stripe response.
Transaction boundaries
| Transaction | Writes |
|---|---|
| Create booking | User if needed, booking, order, order item, optional referral attribution, optional reward reservation, idempotency key. |
| Verified payment | Webhook event, payment status, order PAID, booking PENDING_APPROVAL, pending reward row, notification, email job, audit. |
| Approve | Booking CONFIRMED, reward credit if eligible, notification, audit. |
| Reject or cancel | Booking terminal status, order REFUND_PENDING, reward reversal if a credit exists, refund job, audit. |
Provider HTTP calls sit outside those transactions. The job row is the record that the call still needs to happen.