Skip to main content

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_cents times 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_events with UNIQUE(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_APPROVAL and the order is PAID.
  • Approval sets CONFIRMED, approved_by, and approved_at.
  • Rejection of a paid booking sets REJECTED, sets the order to REFUND_PENDING, and enqueues payment.refund for 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_PAYMENT and the payment row FAILED. The customer retries with a new payment row on the same order.
  • If a webhook never arrives, payment.reconcile reads 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​

TransactionWrites
Create bookingUser if needed, booking, order, order item, optional referral attribution, optional reward reservation, idempotency key.
Verified paymentWebhook event, payment status, order PAID, booking PENDING_APPROVAL, pending reward row, notification, email job, audit.
ApproveBooking CONFIRMED, reward credit if eligible, notification, audit.
Reject or cancelBooking 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.