# Inventory

Inventory is tracked per Product and active physical branch. The authoritative balance fields are
`onHand`, `reserved`, and `available`, where `available = max(0, onHand - reserved)`. This page
covers the Inventory API, invoice and POS stock behavior, and recovery procedures.

## Overview

- **Balances** show On Hand, Reserved, Available, and stock condition by active physical branch.
- **Movements** record manual Quantity In and Quantity Out for tracked Products.
- **Stocktakes** apply physical counts to branch balances.
- **Suppliers** can be attached to incoming movements.
- **Activity** records manual changes, counts, and automated sales.

### Authentication

All `/api/v1/inventory` endpoints are admin REST endpoints. Send the headers described in
[Invoices](./invoices.md#authentication). Read endpoints require `invoices.view`; write and
recovery endpoints require `invoices.manage`. The merchant scope comes from the authenticated
request. A `locationId` used for an inventory operation must be an active physical branch UUID
owned by that merchant. Quantities support up to three decimal places.

The matching requests are in **Admin API / Invoicing / Inventory** in the Postman collection.

### Common errors

| Status | Cause |
| --- | --- |
| `400` | Invalid input, unavailable Product, inactive branch, or insufficient Available stock. |
| `401` | Missing or invalid authentication. |
| `403` | Permission or merchant scope denied. |
| `404` | Record is not found in the merchant scope. |
| `409` | Another inventory operation currently holds the branch; retry later. |

## Inventory endpoints

### List inventory balances

#### Endpoint

```http
GET /api/v1/inventory/balances
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/balances?productId=PRODUCT_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Query parameters

| Field | Description |
| --- | --- |
| `productId` | Optional Product filter. Product-scoped results include zero-balance active branches. |
| `locationId` | Optional physical branch filter. |
| `includeHistorical` | Set `true` to include persisted inactive-branch balances (read-only). |

#### Response

`200 OK`: `data[]` contains `productId`, `locationId`, `onHand`, `reserved`, `available`, and
`stockCondition`. Product-scoped responses include aggregate `meta.totals`.

```json
{
  "success": true,
  "data": [{
    "productId": "PRODUCT_ID", "locationId": "LOCATION_ID", "onHand": 20,
    "reserved": 3, "available": 17, "stockCondition": "in_stock"
  }],
  "meta": { "total": 1, "totals": { "onHand": 20, "reserved": 3, "available": 17 } }
}
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Invalid merchant scope. |
| `403` | Missing `invoices.view` or merchant access denied. |

### Create a stock movement

#### Endpoint

```http
POST /api/v1/inventory/movements
```

```bash
curl -X POST "https://uat.auxcore.net/api/v1/inventory/movements" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"type":"quantity_in","locationId":"LOCATION_ID","date":"2026-10-28","reason":"Supplier delivery","lines":[{"productId":"PRODUCT_ID","quantity":12.5}]}'
```

#### Request

| Field | Required | Description |
| --- | --- | --- |
| `type` | Yes | `quantity_in` or `quantity_out`. |
| `locationId`, `date`, `reason` | Yes | Active physical branch UUID, ISO date, nonblank reason. |
| `lines` | Yes | Nonempty unique `{ productId, quantity }` entries for tracked Products. |
| `supplierId`, `reference`, `note` | No | Movement metadata. |
| `idempotencyKey` | No | 1–128 letters, digits, `.`, `_`, `:`, or `-`; retry returns original movement. |

```json
{
  "type": "quantity_in",
  "locationId": "2d8b4e17-ec2e-41f2-b6e9-6a32c5fd1780",
  "date": "2026-10-28",
  "reason": "Supplier delivery",
  "supplierId": "supplier-uuid",
  "idempotencyKey": "delivery:po-1042",
  "lines": [{ "productId": "product-uuid", "quantity": 12.5 }]
}
```

#### Response

`201 Created`: `data` contains the saved `movement` and `lines`.

```json
{
  "success": true,
  "data": {
    "movement": { "id": "MOVEMENT_ID", "type": "quantity_in", "locationId": "LOCATION_ID", "reason": "Supplier delivery" },
    "lines": [{ "productId": "PRODUCT_ID", "quantity": 12.5, "before": 0, "after": 12.5 }]
  }
}
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Invalid movement fields, tracked Product, branch, supplier, or idempotency key. |
| `403` | Missing `invoices.manage` or merchant access denied. |
| `400` | Quantity Out exceeds Available or request validation fails. |
| `409` | Branch lease is held by another inventory operation. |

### List stock movements

#### Endpoint

```http
GET /api/v1/inventory/movements
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/movements?productId=PRODUCT_ID&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Query parameters

| Field | Description |
| --- | --- |
| `productId`, `locationId`, `supplierId` | Optional filters. |
| `type` | `quantity_in` or `quantity_out`. |
| `page`, `limit` | Defaults `1` / `10`; maximum limit `200`. |

#### Response

`200 OK`: `data` is the movement list; `meta` contains `page`, `limit`, and `total`.

```json
{ "success": true, "data": [], "meta": { "page": 1, "limit": 10, "total": 0 } }
```

### Get a stock movement

#### Endpoint

```http
GET /api/v1/inventory/movements/:id
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/movements/MOVEMENT_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Response

`200 OK`: `data` contains the movement and its lines.

```json
{ "success": true, "data": { "movement": { "id": "MOVEMENT_ID" }, "lines": [] } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `404` | Movement is not found in the merchant scope. |

### Count stock

#### Endpoint

```http
POST /api/v1/inventory/stocktakes
```

```bash
curl -X POST "https://uat.auxcore.net/api/v1/inventory/stocktakes" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"locationId":"LOCATION_ID","name":"Monthly count","date":"2026-10-28","lines":[{"productId":"PRODUCT_ID","counted":25}]}'
```

#### Request

| Field | Required | Description |
| --- | --- | --- |
| `locationId`, `name`, `date` | Yes | Active physical branch, stocktake name, ISO date. |
| `lines` | Yes | Nonempty unique `{ productId, counted }` entries for tracked Products. |
| `note` | No | Stocktake note. |

Creation applies the count immediately; `counted` cannot be below Reserved.

#### Response

`201 Created`: `data` contains the completed `stocktake` and `lines`.

```json
{
  "success": true,
  "data": {
    "stocktake": { "id": "STOCKTAKE_ID", "status": "completed", "locationId": "LOCATION_ID" },
    "lines": [{ "productId": "PRODUCT_ID", "expected": 20, "counted": 25, "difference": 5 }]
  }
}
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Invalid input or count below Reserved. |
| `403` | Missing `invoices.manage` or merchant access denied. |

### List stocktakes

#### Endpoint

```http
GET /api/v1/inventory/stocktakes
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/stocktakes?locationId=LOCATION_ID&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Query parameters

| Field | Description |
| --- | --- |
| `locationId` | Optional physical branch filter. |
| `status` | `draft` or `completed`. |
| `page`, `limit` | Defaults `1` / `10`; maximum limit `200`. |

#### Response

`200 OK`: `data` is the stocktake list; `meta` contains `page`, `limit`, and `total`.

```json
{ "success": true, "data": [], "meta": { "page": 1, "limit": 10, "total": 0 } }
```

### Get a stocktake

#### Endpoint

```http
GET /api/v1/inventory/stocktakes/:id
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/stocktakes/STOCKTAKE_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Response

`200 OK`: `data` contains the stocktake and its lines.

```json
{ "success": true, "data": { "stocktake": { "id": "STOCKTAKE_ID" }, "lines": [] } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `404` | Stocktake is not found in the merchant scope. |

### Complete a stocktake

#### Endpoint

```http
POST /api/v1/inventory/stocktakes/:id/complete
```

```bash
curl -X POST "https://uat.auxcore.net/api/v1/inventory/stocktakes/STOCKTAKE_ID/complete" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"lines":[{"productId":"PRODUCT_ID","counted":25}]}'
```

#### Request

| Field | Required | Description |
| --- | --- | --- |
| `lines` | Yes | Existing stocktake lines as `{ productId, counted }`. |

#### Response

`200 OK`: `data` contains the completed stocktake and lines.

```json
{ "success": true, "data": { "stocktake": { "id": "STOCKTAKE_ID", "status": "completed" }, "lines": [] } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Already completed, stale balance, invalid line, or count below Reserved. |
| `404` | Stocktake is not found in the merchant scope. |

### Suppliers

### Create a supplier

#### Endpoint

```http
POST /api/v1/inventory/suppliers
```

```bash
curl -X POST "https://uat.auxcore.net/api/v1/inventory/suppliers" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"name":"Example Supply Co","email":"orders@example.com","phone":"5550100"}'
```

#### Request

| Field | Required | Description |
| --- | --- | --- |
| `name` | Yes | Supplier name. |
| `contact`, `email`, `phone`, `address`, `notes` | No | Supplier details. |

#### Response

`201 Created`: `data` is the created supplier.

```json
{ "success": true, "data": { "id": "SUPPLIER_ID", "name": "Example Supply Co", "email": "orders@example.com", "phone": "5550100" } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Supplier name is empty. |
| `403` | Missing `invoices.manage` or merchant access denied. |

### List suppliers

#### Endpoint

```http
GET /api/v1/inventory/suppliers
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/suppliers?search=Example&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Query parameters

| Field | Description |
| --- | --- |
| `search` | Matches name, contact, or email. |
| `page`, `limit` | Defaults `1` / `10`; maximum limit `200`. |

#### Response

`200 OK`: `data` is the supplier list; `meta` contains `page`, `limit`, and `total`.

```json
{ "success": true, "data": [], "meta": { "page": 1, "limit": 10, "total": 0 } }
```

### Get a supplier

#### Endpoint

```http
GET /api/v1/inventory/suppliers/:id
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/suppliers/SUPPLIER_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Response

`200 OK`: `data` is the scoped supplier.

```json
{ "success": true, "data": { "id": "SUPPLIER_ID", "name": "Example Supply Co", "email": "orders@example.com" } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `404` | Supplier is not found in the merchant scope. |

### Update a supplier

#### Endpoint

```http
PUT /api/v1/inventory/suppliers/:id
```

```bash
curl -X PUT "https://uat.auxcore.net/api/v1/inventory/suppliers/SUPPLIER_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{"phone":"5550101"}'
```

#### Request

| Field | Required | Description |
| --- | --- | --- |
| `name`, `contact`, `email`, `phone`, `address`, `notes` | No | Supplied fields are updated; omitted fields are unchanged. |

#### Response

`200 OK`: `data` is the updated supplier.

```json
{ "success": true, "data": { "id": "SUPPLIER_ID", "name": "Example Supply Co", "phone": "5550101" } }
```

#### Errors

| Status | Cause |
| --- | --- |
| `400` | Supplier name is empty. |
| `404` | Supplier is not found in the merchant scope. |

### Delete a supplier

#### Endpoint

```http
DELETE /api/v1/inventory/suppliers/:id
```

```bash
curl -X DELETE "https://uat.auxcore.net/api/v1/inventory/suppliers/SUPPLIER_ID" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Response

`200 OK`: `{ "success": true, "data": { "id": "supplier-uuid" } }`.

#### Errors

| Status | Cause |
| --- | --- |
| `404` | Supplier is not found in the merchant scope. |

### Read inventory activity

#### Endpoint

```http
GET /api/v1/inventory/activity
```

```bash
curl -X GET "https://uat.auxcore.net/api/v1/inventory/activity?productId=PRODUCT_ID&locationId=LOCATION_ID&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

#### Query parameters

| Field | Description |
| --- | --- |
| `productId`, `locationId` | Optional Product and physical branch filters. |
| `type` | `quantity_in`, `quantity_out`, `invoice_sale`, `pos_sale`, `item_count`, `tracking_disabled`, `tracking_enabled`, or `stock_update_pending`. |
| `startDate`, `endDate` | Optional date range. |
| `page`, `limit` | Defaults `1` / `10`; maximum limit `200`. |

#### Response

`200 OK`: `data` is the activity list; `meta` contains `page`, `limit`, and `total`.

```json
{ "success": true, "data": [], "meta": { "page": 1, "limit": 10, "total": 0 } }
```
