> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dr.green/llms.txt
> Use this file to discover all available pages before exploring further.

# Order lifecycle

# Order Lifecycle Guide

> **Audience:** Engineers building order flows in a Dr Green-backed store.
> **TL;DR:** Orders pass through five conceptual stages — placed, admin-reviewed, paid, dispatched, delivered. Each stage updates one of three independent status fields. Polling is your only signal for transitions.

***

## The three status fields

Every order has three independent statuses. They evolve in parallel, not strictly sequentially.

| Field           | Set by                                                    | Values                                                       | Meaning                                       |
| --------------- | --------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------- |
| `orderStatus`   | Dr Green internal logic                                   | `PENDING`, `PROCESSING`, `SHIPPED`, `DELIVERED`, `CANCELLED` | Where the order is in the fulfilment pipeline |
| `adminApproval` | Dr Green admin team                                       | `PENDING`, `VERIFIED`, `REJECTED`                            | Manual review outcome                         |
| `paymentStatus` | Payment processor webhook (CoinRemitter / Payinn / PGPay) | `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `REFUNDED`   | Did the customer pay                          |

**Critical:** these three are independent. An order can be `adminApproval=VERIFIED` while `paymentStatus=PENDING` (admin pre-approves, customer hasn't paid yet) and vice versa. **Always check all three** before considering an order "complete."

🔒 The complete enum lists above are partly inferred. Verified live values: `orderStatus=PENDING|DELIVERED`, `adminApproval=PENDING|VERIFIED|REJECTED`, `paymentStatus=PENDING`. Other values are likely supported but not yet seen in production data — defensively render unknown values as the raw string.

***

## The lifecycle visualised

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                              │
│  POST /dapp/orders                                                           │
│  Your store sends the order request                                          │
│                                                                              │
└──────────────────────────┬──────────────────────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STAGE 1: PLACED                                                              │
│                                                                              │
│   orderStatus    = PENDING                                                   │
│   adminApproval  = PENDING                                                   │
│   paymentStatus  = PENDING                                                   │
│                                                                              │
│   Returned with a fresh `id` and a 32-char `invoiceNumber`                  │
│                                                                              │
└──────────────────────────┬──────────────────────────────────────────────────┘
                           │
                           │  Dr Green admin reviews (manual, hours to days)
                           │
                  ┌────────┴────────┐
                  │                 │
                  ▼                 ▼
┌──────────────────────┐  ┌──────────────────────┐
│ STAGE 2a: REJECTED   │  │ STAGE 2b: APPROVED   │
│ adminApproval=       │  │ adminApproval=       │
│ REJECTED             │  │ VERIFIED             │
│                      │  │                      │
│ TERMINAL.            │  │ Customer prompted to │
│ Refund any payment.  │  │ pay (if not already) │
│ Notify customer.     │  │                      │
└──────────────────────┘  └──────────┬───────────┘
                                     │
                                     │  Customer pays via processor
                                     │
                          ┌──────────┴────────────┐
                          │                       │
                          ▼                       ▼
                 ┌─────────────────┐     ┌─────────────────┐
                 │ STAGE 3: PAID   │     │ STAGE 3': FAILED│
                 │ paymentStatus=  │     │ paymentStatus=  │
                 │ COMPLETED       │     │ FAILED          │
                 └────────┬────────┘     └─────────────────┘
                          │                       │
                          │                       │ Customer can retry
                          ▼                       ▼ payment via Dr Green
              ┌────────────────────────────────────────┐
              │ STAGE 4: DISPATCHED                     │
              │ orderStatus = SHIPPED                   │
              │ (Dr Green ships from their distribution)│
              └────────────────┬────────────────────────┘
                               │
                               ▼
              ┌────────────────────────────────────────┐
              │ STAGE 5: DELIVERED   (TERMINAL)         │
              │ orderStatus = DELIVERED                 │
              │ Commission accrues to the holder        │
              └─────────────────────────────────────────┘
```

***

## Reading the order through this lifecycle

### Stage 1 — placed

Right after `POST /dapp/orders`, every status is `PENDING`:

```json theme={null}
{
  "data": {
    "id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
    "invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
    "orderStatus": "PENDING",
    "adminApproval": "PENDING",
    "paymentStatus": "PENDING",
    "totalAmount": 9.09
  }
}
```

**Your UI:** Show "Order received — awaiting review by Dr Green." Include the `invoiceNumber` as the customer-facing reference (Dr Green generates it; don't make your own).

### Stage 2 — admin review

A Dr Green admin reviews each order before it can proceed. This is a manual step and can take hours to days, especially during business off-hours.

After review, `adminApproval` flips:

| New value  | What it means | Your action                                                                           |
| ---------- | ------------- | ------------------------------------------------------------------------------------- |
| `VERIFIED` | Approved      | Continue to payment if not already paid                                               |
| `REJECTED` | Not approved  | Refund any payment via Dr Green; notify customer with reason if Dr Green provides one |

> 🔒 **Reason for rejection** — whether Dr Green surfaces a reason in the order detail isn't formally documented. Defensively render: if you find a `rejectionReason` field, surface it; otherwise, "Your order could not be approved at this time."

### Stage 3 — payment

Payment is collected via the processor specified in `paymentMethod` at order time:

* `CRYPTO` → CoinRemitter (BTC, ETH, USDT, etc.)
* `FIAT` → Payinn (card / bank transfer depending on country)
* `PGPAY` → PGPay (alternate fiat in supported regions)

> 🔒 **Customer-facing payment UX** — whether Dr Green provides a hosted checkout, sends a payment link via email, or relies on your store to render a checkout isn't formally documented from the API surface. Confirm with Dr Green which model applies. The most common pattern is "Dr Green emails a payment link to the customer" — but verify before building.

When payment completes (the processor's webhook fires to Dr Green), `paymentStatus` flips:

| `paymentStatus` | Meaning                                       |
| --------------- | --------------------------------------------- |
| `COMPLETED`     | Paid; order proceeds to dispatch              |
| `FAILED`        | Payment failed; customer can retry            |
| `PROCESSING`    | In flight (e.g. crypto confirmations pending) |
| `REFUNDED`      | Money returned to customer                    |

### Stage 4 — dispatch

When the order ships, `orderStatus` flips to `SHIPPED`. Dr Green handles the dispatch from their distribution partner (e.g. Takoda1 in the UK).

🔒 **Tracking number visibility** — whether the order detail exposes a courier tracking number or `transactions[]` array entries are not yet captured live. The order detail does include a `transactions` array which is empty in observed data; this may populate with shipping info, payment info, or both.

### Stage 5 — delivered

`orderStatus = DELIVERED` is the terminal happy-path state. At this point, the holder's commission for this order accrues — visible via [`/dapp/commissions`](../reference/commissions.md).

***

## How to detect transitions in your store

There are no outbound webhooks, so polling is your signal. The pattern:

### Per-order polling (active orders)

```typescript theme={null}
async function trackOrder(orderId: string, intervalMs = 60_000) {
  const TERMINAL = new Set(['DELIVERED', 'CANCELLED']);

  let lastFingerprint = '';
  while (true) {
    const { orderDetails: o } = await drGreenClient.get<{
      orderDetails: {
        orderStatus: string;
        adminApproval: string;
        paymentStatus: string;
      };
    }>(`/dapp/orders/${orderId}`);

    const fingerprint = `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`;

    if (fingerprint !== lastFingerprint) {
      await onStatusChange(orderId, o);  // notify customer, update your DB
      lastFingerprint = fingerprint;
    }

    if (TERMINAL.has(o.orderStatus) || o.adminApproval === 'REJECTED') return o;

    // 60s ± 10% jitter
    const jitter = 0.9 + 0.2 * Math.random();
    await new Promise((r) => setTimeout(r, intervalMs * jitter));
  }
}
```

> Note `data.orderDetails` (not `data` directly) — order detail wraps in an extra `orderDetails` field, unlike most other endpoints.

### Bulk polling (all active orders across customers)

For at-scale stores, use `/dapp/orders/recent` as a heartbeat:

```typescript theme={null}
async function heartbeatAllActiveOrders() {
  const recent = await drGreenClient.get<{
    recentOrders: Array<{ id: string; orderStatus: string; adminApproval: string; paymentStatus: string }>;
  }>('/dapp/orders/recent');

  for (const o of recent.recentOrders) {
    const local = await db.orders.find(o.id);
    if (!local) continue;
    if (local.fingerprint !== `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`) {
      await onStatusChange(o.id, o);
      await db.orders.updateFingerprint(o.id, o);
    }
  }
}

// Run every 60s
cron.schedule('* * * * *', heartbeatAllActiveOrders);
```

`recent` returns the most recent \~10 orders without pagination — ideal for catching changes across customers cheaply.

For older orders (>1 day old, not in `recent` anymore), poll them individually but at lower frequency (e.g. every 30 min until terminal).

### Recommended polling cadence

| Order age / state                                             | Cadence      |
| ------------------------------------------------------------- | ------------ |
| Just placed, not yet `VERIFIED`                               | Every 5 min  |
| `VERIFIED`, awaiting payment                                  | Every 5 min  |
| `paymentStatus=COMPLETED`, awaiting `SHIPPED`                 | Every 30 min |
| `SHIPPED`, awaiting `DELIVERED`                               | Every 60 min |
| Terminal (`DELIVERED`, `CANCELLED`, `adminApproval=REJECTED`) | Stop polling |

***

## Customer notifications by transition

Suggested template for what to email/notify the customer at each transition:

| Transition                                 | Notify                                                                           | Tone                 |
| ------------------------------------------ | -------------------------------------------------------------------------------- | -------------------- |
| Order placed                               | "We've received your order. Reference: `<invoiceNumber>`. Awaiting review."      | Confirmation         |
| `adminApproval=VERIFIED`, awaiting payment | "Your order is approved. Please complete payment via the link sent by Dr Green." | Action required      |
| `paymentStatus=COMPLETED`                  | "Payment confirmed. Your order will ship shortly."                               | Confirmation         |
| `orderStatus=SHIPPED`                      | "Your order has been dispatched." (include tracking if available)                | Update               |
| `orderStatus=DELIVERED`                    | "Delivered. Thank you!"                                                          | Closure              |
| `adminApproval=REJECTED`                   | "We were unable to approve this order. Please contact support."                  | Apology + escalation |
| `paymentStatus=FAILED`                     | "Your payment didn't complete. You can retry via the link sent by Dr Green."     | Retry prompt         |

**Important:** these are emails YOUR store sends, separate from any transactional emails Dr Green sends (KYC instructions, payment links). You own the customer relationship; Dr Green handles the parts that require their direct involvement.

***

## Idempotency and retry semantics

`POST /dapp/orders` is **not** idempotent. There's no `Idempotency-Key` header support. Naive retries produce duplicate orders.

### The pattern that works

1. **Generate a correlation ID client-side** at checkout, store it in your DB before the API call:
   ```typescript theme={null}
   const correlationId = crypto.randomUUID();
   await db.orders.insert({ correlationId, customerId, items, status: 'pending_post' });
   ```

2. **POST the order:**
   ```typescript theme={null}
   try {
     const order = await drGreenClient.post('/dapp/orders', { ... });
     await db.orders.update(correlationId, { drGreenOrderId: order.id, status: 'placed' });
   } catch (e) {
     // network error — DON'T blindly retry
     await db.orders.update(correlationId, { status: 'unknown', error: e.message });
   }
   ```

3. **For any retry**, first check Dr Green's side:
   ```typescript theme={null}
   if (order.status === 'unknown') {
     // Look for orders placed in the last 5 minutes for this customer
     const recent = await drGreenClient.get('/dapp/orders', {
       clientId: customer.drGreenClientId,
       page: 1,
       limit: 20,
     });
     const match = recent.orders.find(o =>
       Math.abs(o.totalAmount - expectedTotal) < 0.01 &&
       Date.parse(o.createdAt) > Date.now() - 5 * 60 * 1000
     );
     if (match) {
       // Order DID land — recover state, don't re-POST
       await db.orders.update(correlationId, { drGreenOrderId: match.id, status: 'placed' });
     } else {
       // Order didn't land — safe to re-POST
       const order = await drGreenClient.post('/dapp/orders', { ... });
       await db.orders.update(correlationId, { drGreenOrderId: order.id, status: 'placed' });
     }
   }
   ```

This is verbose but safe. If you find yourself implementing this, please also raise the gap with Dr Green — `Idempotency-Key` support is a reasonable backend ask.

***

## Pricing and currency

The order detail returns prices in two currencies:

* `totalAmount` (USD) — accounting / reporting figure
* `localPrice.totalAmount` (customer's local currency) — what the customer was charged

```json theme={null}
{
  "totalAmount": 9.09,
  "localPrice": {
    "currency": "ZAR",
    "totalAmount": 165,
    "totalProfit": 66,
    "wholeSalePrice": 99,
    "retailPrice": 165
  }
}
```

**For customer-facing UI: always use `localPrice.currency` + `localPrice.totalAmount`.**
**For internal reporting / accounting: use `totalAmount` (USD).**

The exchange rate is captured at order time and frozen for that order — even if FX moves, the customer is charged the locked-in `localPrice.totalAmount`.

***

## Anti-patterns to avoid

| Anti-pattern                                             | Why bad                                                                        |
| -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Treating `orderStatus=PENDING` as "no progress"          | The order is awaiting admin review — that IS progress                          |
| Polling every order every minute                         | Wastes API calls; your customers don't need second-by-second updates           |
| Showing customer the USD `totalAmount`                   | They paid in their local currency; show them `localPrice.totalAmount`          |
| Auto-cancelling orders that sit in `PENDING` for 24h     | Dr Green admin may legitimately take 24+ hours                                 |
| Sending status notification on every poll                | Only notify on **transitions** (use the fingerprint pattern above)             |
| Creating an order, then later POSTing to "update status" | There's no PATCH for orders; status changes are server-side only               |
| Trusting `data` directly on order-detail responses       | It wraps in `orderDetails` — `response.data.orderDetails`, not `response.data` |

***

## Where to next

* [reference/orders.md](../reference/orders.md) — full endpoint detail
* [reference/commissions.md](../reference/commissions.md) — what happens after `DELIVERED`
* [04-errors.md § Idempotency](../04-errors.md#idempotency) — the no-`Idempotency-Key` workaround
* [06-webhooks.md § The polling pattern](../06-webhooks.md#the-polling-pattern) — battle-tested polling at scale
