# Order lifecycle (Partner API)

This document describes the end-to-end flow of an order created via the Vylvet Partner API, from store discovery through delivery or pickup. Every order — partner or native — uses the same lifecycle. There is no special-case path for any individual partner.

## TL;DR

```
discovery → preflight → checkout → paid → processed → fulfilled
```

1. **Discover stores** with `POST /v1/stores/nearby` (delivery or pickup).
2. **Validate** the cart with `POST /v1/checkout/preflight` to surface availability or radius problems before charging the customer.
3. **Charge** via `POST /v1/checkout/session` (Vylvet processes payment) or **report** an externally-paid order via `POST /v1/orders/report`.
4. After Stripe success the order is `paid`. The merchant confirms in CMS — or you call `POST /v1/orders/:order_id/confirm` from your own POS — to advance to `processed`. That is the trigger that **deducts inventory** and **creates the Uber Direct delivery**.
5. Pickup orders proceed `processed → ready_for_pickup → picked_up`. Delivery orders proceed `processed → shipped → out_for_delivery → delivered`.

Every transition above emits a partner webhook to your `callback_url` if one is configured. Use webhooks instead of polling.

---

## 1. Stores and products

### Store mapping
- Orders are **single-store**. Every order references exactly one `store_id`.
- The customer (or your UI) chooses a store via `POST /v1/stores/nearby`. The endpoint honours your API key's `partner_store_ids` restriction (if any) and each store's own `vylvet_delivery_radius_miles`.
- All `items[].product_id` in the order must belong to that `store_id`. `POST /v1/checkout/preflight` rejects mismatches with reason `item_unavailable`.

### Product catalog
- For partners that own product data, push it via `POST /v1/products/upsert` (one product per call, with optional `Idempotency-Key`).
- Inventory adjustments are explicit: `POST /v1/inventory/adjustments` with `{ adjustments: [{ product_id, delta, reason? }] }`. This is in addition to the automatic decrement that happens on the `processed` transition.

## 2. Pre-flight

`POST /v1/checkout/preflight` is the safe-to-retry validator. Always call it before creating an order. It returns:

```json
{ "ok": true, "eligible": true | false, "reasons": ["store_inactive", "item_unavailable", "out_of_radius", "quote_unavailable", ...], "quote": { "fee_cents": 599, "eta_minutes": 28 } }
```

Surfacing these 409-equivalents up front avoids a Stripe session being created for an order we cannot fulfill.

## 3. Order creation

There are two creation paths. Pick the one that matches who is processing payment.

### Flow A — Vylvet processes payment (`POST /v1/checkout/session`)
1. You send cart + addresses + return URLs. Order is created in `pending_payment`. We re-check availability; on conflict you get 409 and no Stripe session.
2. We return `checkout_url`. You redirect the customer to Stripe (or use Stripe Elements with the returned `session_id`).
3. On Stripe success, our `stripe_webhook` flips the order to **`paid`**. We email + SMS the merchant and emit `order.paid`. Inventory is **not yet** deducted.

### Flow B — externally paid (`POST /v1/orders/report`)
1. You created the order in your own system (cash, your own Stripe, etc.) and now mirror it into Vylvet.
2. Requires `Idempotency-Key`. We create the order directly in `processed` and deduct inventory in the **same transaction** — if stock is insufficient we return 409 and do not create the order.
3. We then create the Uber Direct delivery (delivery orders only). You receive `order_id`.

## 4. Merchant acceptance

Flow A leaves the order at `paid`. The merchant must explicitly accept it before we touch inventory or dispatch a courier. Two equivalent paths:

- **Vylvet CMS:** the merchant clicks "Confirm" on the order page. This is the existing fulfillment surface — no separate partner-ops dashboard.
- **Partner POS:** call `POST /v1/orders/:order_id/confirm` from your own POS. Equivalent in every way to the CMS button.

Either path advances the order to `processed`, decrements `available_quantity` for each line item, and (for delivery orders) creates the Uber Direct quote + delivery. We emit `order.processed`.

> Why split `paid` and `processed`? It prevents a courier from being dispatched before the merchant has actually prepared the items. The previous behavior — where partner orders skipped straight to `processed` — meant drivers occasionally arrived to an empty pickup bag.

## 5. Fulfillment

### Delivery orders (`fulfillment_type: 'delivery'`)
- The `processed` transition kicks off Uber Direct: we get a quote, then create the delivery, and store `uber_quote_id`, `uber_delivery_id`, and `uber_tracking_url` on the order. Status becomes `shipped`.
- Driver lifecycle: `shipped → out_for_delivery → delivered`. Each transition is a webhook.
- For real-time visibility, partners can subscribe to webhooks (recommended) or poll `GET /v1/orders/:order_id/driver-location` for current courier lat/lng.
- The Uber Direct webhook also drives our timeline events visible via `GET /v1/orders/:order_id/timeline` and the unified snapshot at `GET /v1/orders/:order_id/tracking`.

### Pickup orders (`fulfillment_type: 'pickup'`)
- The `processed` transition does **not** create a delivery. The order waits for the merchant to mark it ready.
- Merchant marks `ready_for_pickup` in the Vylvet CMS, or POS calls `POST /v1/orders/:order_id/ready-for-pickup`. We email the customer and emit `order.ready_for_pickup`.
- After the customer collects, merchant marks `picked_up` in the CMS, or POS calls `POST /v1/orders/:order_id/picked-up`. We emit `order.picked_up`.

## 6. Cancel and refund

- `POST /v1/orders/:order_id/cancel` (refused in terminal states `delivered`, `picked_up`, `cancelled`, `refunded`) cancels the order and the Uber Direct delivery, and restores inventory if it had already been deducted. Emits `order.cancelled` and `delivery.cancelled`.
- `POST /v1/orders/:order_id/refund` refunds via Stripe. Requires `Idempotency-Key`. Optional `amount_cents` for partial refund. Full refunds set the order to `refunded`. Emits `order.refunded`.
- Orders created via `POST /v1/orders/report` (already paid externally) cannot be refunded through this endpoint — issue the refund in your own payment system and post a `cancel` to mirror the state.

## 7. Webhooks

Configure `callback_url` (and optional `callback_secret`) on your partner client. Status events the trigger fires on every transition:

| Event | When |
|---|---|
| `order.paid` | Stripe success |
| `order.processed` | Merchant confirm (CMS or `/confirm`) |
| `order.shipped` | Uber Direct delivery created |
| `order.out_for_delivery` | Driver picked up the order |
| `order.delivered` | Driver completed the delivery |
| `order.ready_for_pickup` | Merchant marked ready (CMS or `/ready-for-pickup`) |
| `order.picked_up` | Merchant marked picked up (CMS or `/picked-up`) |
| `order.cancelled` | `/cancel` or CMS cancel |
| `order.refunded` | `/refund` completed |

Plus operation-specific events fired during specific calls: `order.accepted` (orders/report), `delivery.created`, `delivery.cancelled`, `delivery.courier_update`.

Webhook signature: `x-partner-signature` header is HMAC-SHA256 of the raw body using your `callback_secret`. The body is JSON: `{ event, order_id, timestamp, trace_id, data: { ... } }`.

## 8. Status reference

```
pending_payment → paid → processed → shipped → out_for_delivery → delivered
                                  ↘ ready_for_pickup → picked_up
                                  ↘ cancelled
                       paid       ↘ cancelled
delivered | picked_up → refunded
```

Terminal states: `delivered`, `picked_up`, `cancelled`, `refunded`. The full transition table is in [shared/utilities/order_status.ts](../shared/utilities/order_status.ts).
