Source document · API/docs/ORDERS_AND_PAYMENTS.md
Orders, booking prices and payments
Orders represent a customer’s booking request. Payments represent attempts to pay for an order or for a provider listing subscription. They are separate records with separate states: creating an order does not charge money, and submitting a payment claim does not mark the order paid.
The booking source of truth
The client sends a menu_item_id, quantity, optional notes, and optional RFC3339 schedule_at. The API looks up the currently active menu item and its parent listing/provider, then calculates total_amount = current unit price × quantity. The client does not get to choose the provider, business, item name snapshot, total, or currency. The service accepts quantity from 1 to 100 and checks multiplication overflow. The selected listing must still be active, subscribed, unexpired, and owned by an approved provider when the order is created.
This protects the transaction from stale or modified client prices. The order stores item and price information so later menu edits do not rewrite the booking’s original amount. An optional schedule is stored as a timestamp; it does not create a separate scheduler or guarantee the provider accepted the time.
Member and guest order creation
POST /api/v1/orders accepts an optional bearer token:
- Signed-in member: customer identity comes from the token. Email and phone are not used to claim the order.
- Guest: with no valid bearer token, the request must include a valid email and phone. A guest must send an
Idempotency-Key. The server returns a one-time random guest access token and stores only its hash. The capability expires after 90 days.
Member idempotency keys are optional at the handler/service boundary but strongly recommended for retryable creates. For guests, the key is required because it also prevents duplicate guest bookings and supports safe capability reissuance. Reusing a key for different order content returns 409 idempotency_conflict; a retry with the same logical payload returns the original result. Keys are scoped to the customer or guest identity.
Guest order reads use GET /api/v1/guest/orders/{id} and the X-Guest-Access-Token header. The capability is a bearer secret: anyone holding it can read that booking. It is not a normal account token and cannot access other routes. Never put it in logs, analytics, referral links, or crash reports.
Order lifecycle and concurrency
The order states are pending, confirmed, in_progress, completed, and cancelled. Allowed transitions are:
POST /api/v1/orders/{id}/transition includes the requested status and current version. The API reloads the authoritative order, checks actor permissions, compares the version, validates the transition, and performs an optimistic update. A stale version returns 409 order_conflict; reload the order and make a new decision instead of blindly retrying the same state change.
- A customer may cancel their own order only while it is pending or confirmed.
- A provider may manage orders belonging to the provider they own. Provider ownership is checked against the stored provider owner relationship.
- An administrator may manage an order through the admin route.
- A normal member cannot confirm, start, complete, or cancel another member’s order.
- Completing an order uses a dedicated PostgreSQL transaction: it updates the order, creates one completed-order snapshot and primary sale, and writes outbox events. Retrying a completion cannot create duplicate snapshots or sales.
GET /api/v1/orders lists the caller’s orders, with limit (default 20, maximum 100) and offset. GET /api/v1/orders/{id} returns a participant/staff-authorized order. These routes do not provide a global order search; use the administrative APIs for operational search.
Payment records and status
Payment attempts have statuses unpaid, payment_submitted, payment_confirmed, payment_rejected, and refunded. Supported payment methods in the current service are wechat_qr and alipay_qr. Although the domain enum contains stripe, the request service accepts only those two manual methods and the router has no Stripe checkout or webhook route. Do not present Stripe as a working client payment flow.
| Endpoint | Purpose |
|---|---|
POST /api/v1/orders/{id}/payments | Create a customer-owned payment attempt for the stored order amount. Requires a supported method and an Idempotency-Key. |
POST /api/v1/listings/{id}/subscription-payments | Create a payment for an owned listing using the configured server subscription amount, currency, and duration. Requires an Idempotency-Key. |
POST /api/v1/payments/{id}/submit | Customer records that they paid out of band. The payment status becomes submitted; this is a claim, not verification. The request uses the current payment version. |
POST /api/v1/payments/{id}/evidence | Attach an authorized media asset and/or note as supporting evidence for the customer’s payment claim. |
POST /api/v1/payments/{id}/confirm | The owning provider or an administrator confirms an order payment. Only an administrator can confirm a listing subscription payment; confirmation activates or extends the subscription in the same transaction. |
POST /api/v1/admin/payments/{id}/reject | Administrator rejects a payment claim. |
The API returns the configured method-specific merchant instructions with payment responses. It does not move money, verify a QR transfer automatically, or infer the merchant recipient. Operators must keep WECHAT_PAY_INSTRUCTIONS and ALIPAY_PAY_INSTRUCTIONS aligned with real merchant accounts. The review action—not the customer’s submit action—is what establishes payment_confirmed.
Payment creation is idempotent by owner and key. Reusing an existing key for a different method/order returns 409. Submission is version-checked and safely replayable; a lost response should be retried with the same logical operation and current version, not by creating a new payment.
How order and payment state relate
Order state answers “what is happening with the booking?” Payment state answers “what is the verification state of this payment attempt?” A payment status is not an order transition. The order response also has a payment status summary, but clients should not use that summary to grant provider work actions or bypass the separate order transition policy.
For listing subscriptions, confirmation also sets the subscription active, advances its expiry by the configured plan duration (starting from the later of now or the existing expiry), and can activate a draft listing if its provider is already approved. For order payment, confirmation changes payment state; it does not automatically bypass the order’s own workflow policy.
Practical retry rules
- Generate one random idempotency key for a user action and persist it until the create request has a definitive result.
- Reuse the same key when retrying after a timeout. Do not generate a fresh key just because the response was lost.
- If the server returns an idempotency conflict, stop: the key was previously bound to a different payload.
- For versioned transition/submission calls, use the version from the latest resource response. A conflict means fetch the latest record and reconcile.
- Never show “paid” based only on client-side confirmation or
payment_submitted.
See authentication and access control for guest token handling and the endpoint reference for each operation’s documented response codes.