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

# Sales

# Sales

> **Endpoint reference: ✅ Verified live against production on 10 May 2026.**
> "Sales" in Dr Green parlance is a lightweight CRM-style pipeline — a way to track customer interest before it becomes an order. Each sale entry has a `stage` (LEADS / ONGOING / CLOSED), is associated with a client, and optionally references the order it eventually became.

***

## Endpoints

| Method  | Path                  | Auth          | Description                                      |
| ------- | --------------------- | ------------- | ------------------------------------------------ |
| `POST`  | `/dapp/sales`         | API-key + sig | Create a new sales pipeline entry                |
| `GET`   | `/dapp/sales`         | API-key + sig | List sales entries, paginated                    |
| `PATCH` | `/dapp/sales`         | API-key + sig | Update an existing sale (stage transition, etc.) |
| `GET`   | `/dapp/sales/summary` | API-key + sig | Counts by stage                                  |

***

## Stage enum

| Stage     | Meaning                                                                 |
| --------- | ----------------------------------------------------------------------- |
| `LEADS`   | Initial interest captured (e.g. consultation booked, enquiry submitted) |
| `ONGOING` | Active conversation, customer is engaged but hasn't ordered             |
| `CLOSED`  | Sale concluded — either won (linked to an order) or lost                |

The `stage` field is set by your store; the platform doesn't auto-promote between stages. Combine with `orderId` to distinguish won vs. lost closures (`orderId` set → won; null → lost).

***

## `POST /dapp/sales` — create a sale entry

Use this when a customer first expresses interest — e.g. they book a consultation, submit a contact form, or have an offline conversation your team logs in your store CRM.

### Request body

Per `createSaleDto` (full schema not formally declared in the spec; verified pattern):

```json theme={null}
{
  "clientId": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
  "stage": "LEADS",
  "description": "Booked initial consultation for chronic pain"
}
```

| Field         | Type          | Required | Notes                                                       |
| ------------- | ------------- | -------- | ----------------------------------------------------------- |
| `clientId`    | string (UUID) | ✅        | Must belong to the calling holder                           |
| `stage`       | string        | ✅        | One of `LEADS`, `ONGOING`, `CLOSED`                         |
| `description` | string        | No       | Free-text notes; visible in the holder's dashboard          |
| `orderId`     | string (UUID) | No       | Set only when transitioning to `CLOSED` with a linked order |

### Canonical payload (for signing)

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

### Response

`201 Created` with `data.id`.

🔒 Full POST response shape not yet captured live (would require a write call against production data — held off pending explicit clearance).

***

## `GET /dapp/sales` — list sales

Paginated list of all sales entries for the holder.

### Query parameters

| Name       | Type          | Default | Notes                                     |
| ---------- | ------------- | ------- | ----------------------------------------- |
| `page`     | integer       | `1`     |                                           |
| `limit`    | integer       | `10`    |                                           |
| `stage`    | string        | —       | Filter by `LEADS`, `ONGOING`, or `CLOSED` |
| `clientId` | string (UUID) | —       | Filter to one client                      |
| `search`   | string        | —       | Free-text on description and client name  |

### Canonical payload

`urlencode(query)` if any params, else `"{}"`.

### Response shape (verified)

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": {
    "sales": [
      {
        "id": "97f09fc5-17dc-4677-8a82-aa886b5bfd03",
        "createdAt": "2025-02-26T05:49:11.465Z",
        "updatedAt": "2025-02-26T05:49:11.465Z",
        "stage": "LEADS",
        "description": null,
        "orderId": null,
        "client": {
          "id": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
          "firstName": "Dheeraj",
          "lastName": "Kumar",
          "email": "dheeraj.k+11@rejolut.com",
          "phoneCountryCode": null,
          "phoneCode": "+91",
          "contactNumber": "6394234342",
          "isActive": true
        }
      }
    ],
    "pageMetaDto": {
      "page": "1", "take": 10, "itemCount": 19,
      "pageCount": 2, "hasPreviousPage": false, "hasNextPage": true
    }
  }
}
```

> 🪲 **`description` is `null` when unset** (not empty string). Handle nullable in your UI.

> 🪲 **No filterable created/updated date range** at the API level — fetch wider and filter client-side if you need a time window.

### Worked example (Node)

```typescript theme={null}
async function listSales(stage?: 'LEADS' | 'ONGOING' | 'CLOSED', page = 1, limit = 10) {
  const q = { page: String(page), limit: String(limit), ...(stage ? { stage } : {}) };
  const payload = canonicalPayload('GET', null, q);
  const headers = authHeaders(API_KEY, SECRET_KEY, payload);
  const url = new URL('https://api.drgreennft.com/api/v1/dapp/sales');
  Object.entries(q).forEach(([k, v]) => url.searchParams.set(k, v));
  const res = await fetch(url, { headers });
  return (await res.json()).data;
}
```

***

## `PATCH /dapp/sales` — update a sale

Move a sale through the pipeline (e.g. `LEADS` → `ONGOING` → `CLOSED`).

### Request body

Per `updateSaleDto`:

```json theme={null}
{
  "id": "97f09fc5-17dc-4677-8a82-aa886b5bfd03",
  "stage": "CLOSED",
  "orderId": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
  "description": "Closed after follow-up call; ordered Femme Fatale 1g"
}
```

| Field         | Type          | Required | Notes                                      |
| ------------- | ------------- | -------- | ------------------------------------------ |
| `id`          | string (UUID) | ✅        | The sale to update                         |
| `stage`       | string        | No       | New stage                                  |
| `orderId`     | string (UUID) | No       | Link to the order this sale converted into |
| `description` | string        | No       | Replaces existing description              |

> ⚠️ **Note this is `PATCH /dapp/sales` (no path param)** — the ID goes in the body, not the URL. Unusual REST shape; mind the path.

### Canonical payload

`JSON.stringify(body)` — compact.

### Response

`200 OK` with `data.id`.

🔒 PATCH response shape not yet captured live.

***

## `GET /dapp/sales/summary` — counts by stage

### Canonical payload

`{}`

### Response shape (verified)

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": {
    "summary": {
      "ONGOING": 16,
      "LEADS": 16,
      "CLOSED": 5,
      "totalCount": 19
    },
    "count": 19
  }
}
```

> 🪲 **`summary.totalCount` and `count` are duplicates** — same value, two fields. Pick one.

> 🪲 **The summary counts don't sum to `totalCount`** in the verified data (16 + 16 + 5 = 37, but totalCount is 19). The `summary` numbers appear to be *per-stage cumulative count over time* rather than *current state*. Confirm with Dr Green which interpretation is intended; document accordingly. 🔒

***

## Common patterns

### Lead → won-order pipeline

```ts theme={null}
// 1. Customer enquires → create LEAD
await createSale({ clientId, stage: 'LEADS', description: enquiry });

// 2. Sales rep follows up → move to ONGOING
await updateSale({ id: saleId, stage: 'ONGOING' });

// 3. Customer orders → close + link
const order = await createOrder({ clientId, ... });
await updateSale({ id: saleId, stage: 'CLOSED', orderId: order.id });
```

### Render a "won vs lost" view

```ts theme={null}
const { sales } = await listSales('CLOSED');
const won  = sales.filter(s => s.orderId !== null);
const lost = sales.filter(s => s.orderId === null);
```

***

## Caching guidance

* Sales list: **30s** TTL. Sales rep activity is real-time-ish.
* Summary: **1m** TTL.

***

## See also

* [Clients](./clients.md) — sales reference clients
* [Orders](./orders.md) — closed sales link to orders via `orderId`
* [Commissions](./commissions.md) — commissions accrue from won sales
