> ## 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.

# Orders

# Orders

> **Endpoint reference: ✅ Verified live against production on 10 May 2026.**
> Orders are the conversion event in your store — when a customer's cart becomes a purchase. The Dr Green admin team manually approves each order before it ships, so expect an asynchronous lifecycle: place → pending → admin-verified → paid → shipped.

***

## Order lifecycle (the high-level flow)

```
[Customer checks out]
    │
    ▼
POST /dapp/orders            ← your store's call
    │
    ▼
order.orderStatus = PENDING
order.adminApproval = PENDING
order.paymentStatus = PENDING
    │
    ▼
[Dr Green admin reviews]      ← manual, hours to days
    │
    ├── REJECTED → adminApproval=REJECTED  (terminal — refund, notify customer)
    └── VERIFIED → adminApproval=VERIFIED
                   ▼
              [Customer pays via processor]
                   ▼
              paymentStatus = COMPLETED
                   ▼
              [Dr Green ships]   (no separate shipped status currently surfaced)
```

There's **no outbound webhook** for status changes — your store must poll. See [06-webhooks.md § The polling pattern](../06-webhooks.md#the-polling-pattern).

***

## Endpoints

| Method | Path                            | Auth          | Description                                        |
| ------ | ------------------------------- | ------------- | -------------------------------------------------- |
| `POST` | `/dapp/orders`                  | API-key + sig | Create a new order from a cart                     |
| `GET`  | `/dapp/orders`                  | API-key + sig | List orders, paginated                             |
| `GET`  | `/dapp/orders/recent`           | API-key + sig | Recently-created orders (heartbeat polling target) |
| `GET`  | `/dapp/orders/summary`          | API-key + sig | Order status counts                                |
| `GET`  | `/dapp/orders/chart-data`       | API-key + sig | Time-series, requires date range                   |
| `GET`  | `/dapp/orders/status-breakdown` | API-key + sig | Status breakdown over date range                   |
| `GET`  | `/dapp/orders/{orderId}`        | API-key + sig | Full order detail                                  |

***

## Status enums

| Field           | Values                                                       | Set by                         |
| --------------- | ------------------------------------------------------------ | ------------------------------ |
| `orderStatus`   | `PENDING`, `PROCESSING`, `SHIPPED`, `DELIVERED`, `CANCELLED` | Server-side based on lifecycle |
| `adminApproval` | `PENDING`, `VERIFIED`, `REJECTED`                            | Dr Green admin                 |
| `paymentStatus` | `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `REFUNDED`   | Payment processor webhook      |

🔒 The complete enum lists above are inferred from observed values (`PENDING`, `VERIFIED`, `REJECTED` confirmed live). Other values are likely supported but not yet captured. Defensively handle unknown statuses — render them as the raw string.

***

## `POST /dapp/orders` — create an order

Creates an order from explicit line items + shipping + (optionally) the cart it came from. The shipping address must be one of the client's `shippings[]` (referenced by `id`).

### Request body

The shape follows `CreateOrderDto`. Verified pattern:

```json theme={null}
{
  "clientId": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
  "shippingId": "167d30ee-52d1-48eb-b501-b1203ad72074",
  "orderLines": [
    { "strainId": "c77dd198-ec9a-4ced-8105-937fde104424", "quantity": 1 }
  ],
  "paymentMethod": "CRYPTO"
}
```

| Field                   | Type          | Required | Notes                                                                                                           |
| ----------------------- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `clientId`              | string (UUID) | ✅        | Must be `adminApproval=VERIFIED`, `isKYCVerified=true`, `isActive=true`                                         |
| `shippingId`            | string (UUID) | ✅        | One of `client.shippings[].id`                                                                                  |
| `orderLines`            | array         | ✅        | At least one line                                                                                               |
| `orderLines[].strainId` | string (UUID) | ✅        |                                                                                                                 |
| `orderLines[].quantity` | integer       | ✅        | Grams; positive integer                                                                                         |
| `paymentMethod`         | string        | ✅        | `CRYPTO` (CoinRemitter), `FIAT` (Payinn), `PGPAY` (PGPay) — confirm with Dr Green which are enabled per country |
| `cartId`                | string (UUID) | No       | Reference to the cart that fed this order, if any                                                               |
| `notes`                 | string        | No       | Free-text                                                                                                       |

### Canonical payload (for signing)

`JSON.stringify(body)` — compact, no whitespace.

### Response

`201 Created` with the order's `id` and `invoiceNumber`. The `invoiceNumber` is a 32-char random string Dr Green generates server-side — use it as your customer-facing reference. Don't generate your own.

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

### Idempotency warning

`POST /dapp/orders` is **not idempotent**. A retry creates a duplicate order. Two patterns to avoid this:

**Pattern 1 — pre-flight check.** Before any retry, query `GET /dapp/orders?page=1&limit=20` and look for a recent order matching the customer + total amount. If found, treat your original POST as having succeeded.

**Pattern 2 — your-side correlation ID.** Generate a UUID at checkout, store `order_in_flight: <uuid>` in your store DB, only POST once. On any retry, check if the previous attempt eventually returned a 2xx (which you'd have logged) before retrying.

See [04-errors.md § Idempotency](../04-errors.md#idempotency) for the full discussion.

### Errors

| Status                                           | Cause                                                        | Fix                                                     |
| ------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------- |
| `400` `["clientId must be a UUID"]`              | Validation                                                   | Validate input                                          |
| `400` "Client not verified" / similar            | Client isn't `VERIFIED + KYC + active`                       | Surface to customer; can't recover                      |
| `400` "Shipping not found for client"            | `shippingId` doesn't belong to this client                   | Re-fetch client and use a valid shipping `id`           |
| `400` "Strain not available in client's country" | Strain `availableCountries` doesn't include client's country | Filter strains to client's country before placing order |

***

## `GET /dapp/orders` — list orders

Paginated list of all orders for the holder.

### Query parameters

| Name            | Type          | Default | Notes                                                    |
| --------------- | ------------- | ------- | -------------------------------------------------------- |
| `page`          | integer       | `1`     |                                                          |
| `limit`         | integer       | `10`    |                                                          |
| `status`        | string        | —       | Filter by `orderStatus`                                  |
| `adminApproval` | string        | —       | Filter by `adminApproval`                                |
| `clientId`      | string (UUID) | —       | Filter to one client (or use `/dapp/client/{id}/orders`) |
| `search`        | string        | —       | Free-text on `invoiceNumber`                             |

### Response shape (verified)

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": {
    "orders": [
      {
        "id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
        "invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
        "orderStatus": "PENDING",
        "adminApproval": "VERIFIED",
        "paymentStatus": "PENDING",
        "totalAmount": 9.09,
        "shipping": {
          "country": "South Africa",
          "countryCode": "ZAF",
          "currency": null
        },
        "transactions": [],
        "createdAt": "2025-05-12T17:29:57.100Z",
        "nft": { "tokenId": 56, "ownerId": "0x553374bd..." },
        "client": {
          "id": "f1281910-f463-4da9-92c0-feb81a9db994",
          "firstName": "Jacques",
          "lastName": "De Reuck",
          "shippings": [ { "countryCode": "ZAF" } ]
        },
        "_count": { "orderLines": 1 },
        "totalQuantity": 1,
        "totalPrice": 9.09,
        "localPrice": { "currency": "ZAR", "totalAmount": 165 }
      }
    ],
    "pageMetaDto": { "page": "1", "take": 10, "itemCount": 21, "pageCount": 3, "hasPreviousPage": false, "hasNextPage": true }
  }
}
```

> 🪲 **`_count`** is a Prisma-leaked field — it's `{ orderLines: <count> }`. Treat it as informational, not authoritative.

> 🪲 **`totalAmount`, `totalPrice`, and `localPrice.totalAmount` are different things:**
>
> * `totalAmount` and `totalPrice` are the same USD figure (mirroring of fields)
> * `localPrice.totalAmount` is the customer-currency equivalent (e.g. ZAR for South Africa)
>   Use `localPrice` for display, `totalAmount` for accounting.

***

## `GET /dapp/orders/recent` — recent orders

Same shape as `GET /dapp/orders` but always returns the most recent 10 (no pagination needed). Use this as a heartbeat polling target — cheap call that surfaces anything new across all customers.

### Canonical payload

`{}`

***

## `GET /dapp/orders/summary` — status counts

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": {
    "summary": {
      "PENDING": 0,
      "VERIFIED": 11,
      "REJECTED": 10,
      "totalCount": 21
    }
  }
}
```

The keys are `adminApproval` values (not `orderStatus`). For an `orderStatus` breakdown over time, use `/dapp/orders/status-breakdown` with a date range.

***

## `GET /dapp/orders/chart-data` and `/status-breakdown`

Both require `startDate` and `endDate` in `YYYY-MM-DD` format. Without them: `400`.

### Canonical payload (with date range)

`startDate=2026-04-01&endDate=2026-05-01`

🔒 Response shapes pending live capture with valid date ranges.

***

## `GET /dapp/orders/{orderId}` — order detail

Full detail including line items with embedded strain info, transactions, and the multi-currency price breakdown.

### Canonical payload

`{}`

### Response shape (verified)

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": {
    "orderDetails": {
      "id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
      "invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
      "orderStatus": "PENDING",
      "paymentStatus": "PENDING",
      "totalAmount": 9.09,
      "deliveryFee": 6,
      "totalProfit": 3.59,
      "totalOrdered": 1,
      "totalQuantity": 1,
      "createdAt": "2025-05-12T17:29:57.100Z",
      "updatedAt": "2025-05-13T13:05:08.284Z",
      "transactions": [],
      "nft": { "tokenId": 56, "ownerId": "0x553374bd..." },
      "client": {
        "id": "f1281910-f463-4da9-92c0-feb81a9db994",
        "firstName": "Jacques",
        "lastName": "De Reuck",
        "phoneCode": "+27",
        "contactNumber": "832854483",
        "shippings": [ /* full shipping details */ ]
      },
      "orderLines": [
        {
          "quantity": 1,
          "strain": {
            "id": "c77dd198-ec9a-4ced-8105-937fde104424",
            "name": "Femme Fatale",
            "imageUrl": "33eac80b-58c4-46d3-a82b-b70c875d333f-cakes-and-cream.png"
          }
        }
      ],
      "localPrice": {
        "currency": "ZAR",
        "totalAmount": 165,
        "totalProfit": 66,
        "wholeSalePrice": 99,
        "retailPrice": 165,
        "location": {
          "country": "South Africa",
          "currency": "ZAR"
        }
      }
    }
  }
}
```

> ⚠️ **The order detail wraps the data inside an extra `orderDetails` field** — i.e. `data.orderDetails`, not `data` directly. Other endpoints don't wrap like this. **Mind the inconsistency.**

> 🪲 **`totalAmount` is in USD, `localPrice.totalAmount` is in the customer's local currency.** Don't render `totalAmount` to the customer — use `localPrice.totalAmount` with `localPrice.currency`.

> 🪲 **`totalOrdered` and `totalQuantity` are the same number** in observed responses. The duplication is a backend wart.

***

## Common patterns

### Polling order status

```python theme={null}
async def poll_order_until_terminal(order_id: str):
    terminal = {"DELIVERED", "CANCELLED"}
    while True:
        order = await get_order(order_id)
        details = order["orderDetails"]
        if details["orderStatus"] in terminal or details["adminApproval"] == "REJECTED":
            return details
        await asyncio.sleep(60 + random.uniform(-6, 6))  # 60s ± 10% jitter
```

### Surface multi-currency totals

```ts theme={null}
function displayTotal(order: OrderDetails): string {
  const lp = order.localPrice;
  if (lp?.currency && lp.totalAmount != null) {
    return new Intl.NumberFormat(undefined, { style: 'currency', currency: lp.currency }).format(lp.totalAmount);
  }
  return `$${order.totalAmount.toFixed(2)} USD`;
}
```

### Detecting status changes between polls

```ts theme={null}
function statusFingerprint(o: OrderDetails): string {
  return `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`;
}
// Fire customer notifications only when fingerprint changes
```

***

## Caching guidance

| Resource                     | TTL      | Rationale                         |
| ---------------------------- | -------- | --------------------------------- |
| Active order detail          | 30s      | Customer expects \~real-time feel |
| Terminal-status order detail | 5m       | Doesn't change                    |
| Order list (paginated)       | No cache | Always fresh                      |
| Order summary counts         | 1m       | Dashboards                        |

***

## See also

* [Carts](./carts.md) — orders are typically born from carts
* [Clients](./clients.md) — orders are scoped to clients
* [Strains](./strains.md) — `orderLines[].strain` fields and pricing snapshot
* [Sales](./sales.md) — orders may be associated with a sales pipeline entry
* [06-webhooks.md](../06-webhooks.md) — why you have to poll
