# Time Slot Supplier Inventory — API reference (cURL)

Buy-side supplier rates keyed by **time slot** (`activity_time_slot_supplier_inventory`),
the counterpart of the existing category-keyed `/inventory/*_activity_supplier_inventory`
endpoints (`activity_supplier_inventory`).

| | Category APIs (existing) | Time slot APIs (these) |
|---|---|---|
| Table | `activity_supplier_inventory` | `activity_time_slot_supplier_inventory` |
| Second dimension | `category_id` | `from_time` + `to_time` |
| `group_key` points at | `activity_inventory.group_key` | `time_slot.group_key` |
| Unique key | `activity_id, group_key, activity_date, supplier_id, category_id` | `activity_id, activity_date, from_time, to_time, supplier_id` |

Run the migration first:

```bash
mysql -u <user> -p <database> \
  < migrations/2026_09_13_activity_time_slot_supplier_inventory.sql
```

---

## Setup

```bash
export BASE_URL="http://localhost:5558"     # PORT in .env; use the real host on staging/prod
export TOKEN="<jwt>"                        # same admin JWT the other /inventory/* routes take
```

All four are `POST`, `Content-Type: application/json`, and return HTTP **200** for both
success and business errors — the outcome is in `replyCode` (`"success"` / `"error"`),
matching the existing inventory endpoints. Only a connection failure returns 500.

> **Encryption.** With `ENCRYPTION=false` (current `.env`) responses are plain JSON.
> `DECRYPT_REQUESTS=true` only means the middleware *will* decrypt a base64 body if it
> gets one; a plain JSON body falls through untouched, so the cURL below works as-is.

> **Auth is not enforced.** These routes resolve the JWT to fill `created_by` / `updated_by`
> but do not reject a missing or invalid token — identical to the category APIs they mirror.
> Treat that as inherited behaviour, not a guarantee. Worth gating before this goes anywhere
> public.

---

## Getting a `group_key`

Every call needs the `group_key` of the time slot block. Three ways to get it:

```bash
# a) create_time_slot returns them for the block it just wrote
curl -s -X POST "$BASE_URL/time_slot/create_time_slot" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
        "ref_from": "activity",
        "ref_id": 412,
        "from_date": "2026-10-01",
        "to_date": "2026-12-31",
        "week_days": { "Monday": 1, "Tuesday": 1, "Wednesday": 1, "Thursday": 1, "Friday": 1 },
        "timeSlotsArray": [ { "from_time": "10:00", "to_time": "11:00", "qty": 20 } ]
      }'
# -> { "group_keys": ["activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00"], ... }

# b) list existing blocks
curl -s -X POST "$BASE_URL/time_slot/time_slot_group_list" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "ref_from": "activity", "ref_id": 412, "page": 1, "limit": 20 }'
```

Format: `<ref_from>_<ref_id>_<from_date>_<to_date>_<from_time>_<to_time>`, e.g.
`activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00`. **One block's 10:00 slot and its
14:00 slot are different group_keys** — that is the main difference from the category APIs,
where one `group_key` covers every category.

---

## 1. Add — `POST /inventory/add_activity_time_slot_supplier_inventory`

Writes one row per **existing** slot in the group, per supplier. Upserts on the unique key.

```bash
curl -s -X POST "$BASE_URL/inventory/add_activity_time_slot_supplier_inventory" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
        "supplier_cancellation_policy_id": 5,
        "supplier_inventory": [
          {
            "supplier_id": 9,
            "supplier": {
              "supplier_name": "Desert Tours LLC",
              "supplier_company_name": "Desert Tours LLC",
              "supplier_email": "ops@deserttours.example",
              "supplier_mobile": "+971500000000"
            },
            "currency": "AED",
            "adult_purchase_price": 100.00,
            "child_purchase_price": 60.00,
            "infant_purchase_price": 0.00,
            "total_quantity": 20,
            "available_quantity": 20,
            "priority": 1,
            "status": 1
          }
        ]
      }'
```

**Success**

```json
{
  "replyCode": "success",
  "replyMsg": "Time slot supplier inventory saved successfully.",
  "total_rows": 65,
  "affected_rows": 65
}
```

`total_rows` = rows built from the slots that actually exist. `affected_rows` is MySQL's
upsert count: **1 per insert, 2 per row actually changed, 0 per row already identical**, so
it legitimately exceeds `total_rows` on a re-save. Use `total_rows` for "how many slots did
this cover".

### Body

| Field | Where | Req. | Notes |
|---|---|---|---|
| `activity_id` | root | yes | Must match `time_slot.ref_id` where `ref_from = 'activity'` |
| `group_key` | root | — | Default for entries that omit their own |
| `supplier_cancellation_policy_id` | root | no | Fallback when an entry omits it |
| `supplier_inventory[]` | root | yes | Non-empty |
| `group_key` | entry | yes* | Overrides the root one. *Required at one level or the other |
| `supplier_id` | entry | yes | |
| `currency` | entry | yes | `NOT NULL` in the table — per supplier contract, rows in one response can differ |
| `supplier` | entry | no | JSON snapshot, frozen at write time so a later edit to the supplier master doesn't relabel historic rates |
| `adult_purchase_price` / `child_` / `infant_` | entry | no | Default `0.00` |
| `total_quantity` | entry | no | Default `0` |
| `available_quantity` | entry | no | Defaults to `total_quantity` |
| `priority` | entry | no | Default `1`. Lower = higher priority |
| `status` | entry | no | `0` or `1`, default `1` |
| `supplier_cancellation_policy_id` | entry | no | Falls back to root |
| `from_date` / `to_date` | entry | no | **Narrows** within the group, `YYYY-MM-DD` |
| `weekdays` | entry | no | **Narrows** further: `[{"day":"Saturday","available":false}]`. Omitted or empty = all days |

### The dates come from the database, not the payload

Unlike `add_activity_supplier_inventory`, which expands `from_date..to_date` itself, this
endpoint **reads the real slot dates back from `time_slot` for the group_key** and writes a
row only where a slot exists. `from_date` / `to_date` / `weekdays` can only subtract from
that set. So a rate is never written for a date that has no slot, and a `group_key` matching
nothing is an error rather than a silent success.

### Multiple suppliers and narrowing

```bash
curl -s -X POST "$BASE_URL/inventory/add_activity_time_slot_supplier_inventory" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "supplier_inventory": [
          {
            "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
            "supplier_id": 9,
            "currency": "AED",
            "adult_purchase_price": 100.00,
            "total_quantity": 20,
            "priority": 1,
            "weekdays": [
              { "day": "Saturday", "available": false },
              { "day": "Sunday",   "available": false }
            ]
          },
          {
            "group_key": "activity_412_2026-10-01_2026-12-31_14:00:00_15:00:00",
            "supplier_id": 11,
            "currency": "USD",
            "adult_purchase_price": 28.50,
            "total_quantity": 15,
            "priority": 2,
            "from_date": "2026-11-01",
            "to_date": "2026-12-31"
          }
        ]
      }'
```

Two entries naming the same slot **and** the same supplier: the last one wins (silently),
rather than the two fighting over one key inside a single INSERT.

### Errors

| `replyMsg` | Cause |
|---|---|
| `activity_id is required` | missing |
| `supplier_inventory is required` | missing / empty / not an array |
| `group_key is required (at the root, or per supplier_inventory entry)` | neither level supplied it |
| `supplier_id is required in supplier_inventory` | entry missing it |
| `currency is required in supplier_inventory` | entry missing it |
| `Invalid from_date "01-10-2026" in supplier_inventory, use YYYY-MM-DD` | wrong format, or an impossible date like `2026-02-30` |
| `from_date cannot be after to_date in supplier_inventory` | inverted range |
| `Invalid status in supplier_inventory. Allowed values are 0, 1` | entry `status` not 0/1 |
| `Invalid group_key for this activity: <keys>` | key belongs to another activity, to a sub-activity, or to a block since replaced |
| `No time slots matched the given date range / weekday filter` | narrowing excluded everything — nothing was written |

---

## 2. Update — `POST /inventory/update_activity_time_slot_supplier_inventory`

Two targeting modes.

**By id (one row):**

```bash
curl -s -X POST "$BASE_URL/inventory/update_activity_time_slot_supplier_inventory" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "id": 1042,
        "adult_purchase_price": 115.00,
        "child_purchase_price": 70.00,
        "available_quantity": 12,
        "priority": 1
      }'
```

**By group (bulk), optionally narrowed:**

```bash
curl -s -X POST "$BASE_URL/inventory/update_activity_time_slot_supplier_inventory" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
        "supplier_id": 9,
        "from_date": "2026-12-01",
        "to_date": "2026-12-31",
        "adult_purchase_price": 130.00,
        "priority": 2
      }'
```

```json
{
  "replyCode": "success",
  "replyMsg": "Time slot supplier inventory updated successfully",
  "affected_rows": 31
}
```

### Targeting

`id`, **or** `activity_id` + `group_key`. In group mode these narrow further and are
**filters, never values written**:

| Filter | Meaning |
|---|---|
| `supplier_id` | that supplier only |
| `from_date` / `to_date` | `activity_date >= / <=` |
| `filter_activity_date` | one exact date |
| `filter_from_time` / `filter_to_time` | one exact slot time |

The `filter_` prefixes exist because the unprefixed names are rejected — see below.

### Updatable fields

`currency`, `adult_purchase_price`, `child_purchase_price`, `infant_purchase_price`,
`available_quantity`, `total_quantity`, `priority`, `status` (0/1/2),
`supplier` (JSON), `supplier_cancellation_policy_id`.

`supplier_id` is updatable **only with `id`**. A group-wide supplier swap would try to write
one supplier's rate onto every date in the group at once and collide with
`uk_ts_supplier_inventory` wherever that supplier already has a rate, failing the statement
partway. Passing `supplier_id` alongside `supplier` with an `id` also rewrites the `supplier`
JSON, so the snapshot stays in step.

### Not updatable — by design

`activity_date`, `from_time`, `to_time` are the link to `time_slot` and part of the unique
key. Sending any of them returns an **error rather than silently ignoring them**:

```json
{
  "replyCode": "error",
  "replyMsg": "from_time cannot be updated - they identify the time slot. Use add_activity_time_slot_supplier_inventory for the target slot instead."
}
```

To move a rate to a different slot: `add_` against the target group (which validates the slot
exists), then set the old row to `status: 2`.

### Errors

| `replyMsg` | Cause |
|---|---|
| `Either id OR activity_id + group_key is required.` | no target |
| `<fields> cannot be updated - they identify the time slot...` | sent `activity_date` / `from_time` / `to_time` |
| `No fields to update` | target given, nothing to set |
| `Invalid status. Allowed values are 0, 1, 2` | bad `status` |
| `Invalid from_date, use YYYY-MM-DD` (and `to_date`, `filter_activity_date`) | bad filter date |
| `Invalid filter_from_time, use HH:mm or HH:mm:ss` (and `filter_to_time`) | bad filter time |
| `Time slot supplier inventory not found` | 0 rows matched |

---

## 3. Update status — `POST /inventory/update_activity_time_slot_supplier_inventory_status`

`0` = Inactive, `1` = Active, `2` = Deleted (soft).

```bash
# whole block for one supplier
curl -s -X POST "$BASE_URL/inventory/update_activity_time_slot_supplier_inventory_status" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
        "supplier_id": 9,
        "status": 0
      }'

# one row
curl -s -X POST "$BASE_URL/inventory/update_activity_time_slot_supplier_inventory_status" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "id": 1042, "status": 2 }'

# one date + one slot time across every supplier on that activity
curl -s -X POST "$BASE_URL/inventory/update_activity_time_slot_supplier_inventory_status" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "activity_date": "2026-12-25",
        "from_time": "10:00",
        "status": 0
      }'
```

```json
{
  "replyCode": "success",
  "replyMsg": "Inventory status updated successfully",
  "affected_rows": 4
}
```

`status` is required. **At least one of `id` / `activity_id` / `group_key` / `supplier_id`**
is required — without it the UPDATE would have no WHERE and flip the whole table. Optional
extra filters: `activity_date`, `from_time`, `to_time` (times accept `HH:mm` or `HH:mm:ss`).

| `replyMsg` | Cause |
|---|---|
| `id or activity_id or group_key or supplier_id is required` | no filter |
| `Invalid status. Allowed values are 0, 1, 2` | bad/missing status |
| `Invalid activity_date, use YYYY-MM-DD` / `Invalid from_time, use HH:mm or HH:mm:ss` | bad filter |
| `Inventory not found` | 0 rows matched |

---

## 4. Get — `POST /inventory/get_activity_time_slot_supplier_inventory`

```bash
curl -s -X POST "$BASE_URL/inventory/get_activity_time_slot_supplier_inventory" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "activity_id": 412,
        "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
        "from_date": "2026-10-01",
        "to_date": "2026-10-31",
        "status": 1,
        "page": 1,
        "limit": 20
      }'
```

```json
{
  "replyCode": "success",
  "replyMsg": "Time slot supplier inventory fetched successfully",
  "total_records": 65,
  "total_pages": 4,
  "current_page": 1,
  "page_size": 20,
  "data": [
    {
      "id": 1042,
      "activity_id": 412,
      "group_key": "activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00",
      "activity_date": "2026-10-01",
      "from_time": "10:00:00",
      "to_time": "11:00:00",
      "supplier_id": 9,
      "supplier": { "supplier_name": "Desert Tours LLC" },
      "currency": "AED",
      "adult_purchase_price": "100.00",
      "child_purchase_price": "60.00",
      "infant_purchase_price": "0.00",
      "available_quantity": 20,
      "total_quantity": 20,
      "priority": 1,
      "status": 1,
      "created": "2026-09-13 18:22:04",
      "created_by": { "id": 7, "name": "Darpan", "email": "...", "role": 1 },
      "updated": null,
      "updated_by": null,
      "supplier_cancellation_policy_id": 5,
      "supplier_cancellation_policy_name": "48h free cancellation"
    }
  ]
}
```

| Param | Req. | Notes |
|---|---|---|
| `activity_id` | one of | `activity_id` **or** `group_key` required |
| `group_key` | one of | |
| `supplier_id` | no | |
| `from_date` / `to_date` | no | `YYYY-MM-DD`, inclusive |
| `from_time` / `to_time` | no | exact match, `HH:mm` or `HH:mm:ss` |
| `status` | no | `0` / `1` / `2` |
| `page` | no | default `1` |
| `limit` | no | default `20` |

Ordered by `group_key, activity_date, from_time, priority, supplier_id`.
`supplier_cancellation_policy_name` is a LEFT JOIN onto
`fdk_holidays.supplier_cancellation_policies` — `null` when unset or when the policy row is gone.

Dates come back as strings (`dateStrings` is on for the pool) and `DECIMAL` prices as
strings — parse them, don't compare them as numbers by accident.

| `replyMsg` | Cause |
|---|---|
| `activity_id or group_key is required` | neither given |
| `Invalid from_date, use YYYY-MM-DD` / `Invalid from_time, use HH:mm or HH:mm:ss` | bad filter |

---

## End-to-end smoke test

```bash
export BASE_URL="http://localhost:5558"
export TOKEN="<jwt>"
export ACT=412
export GK="activity_412_2026-10-01_2026-12-31_10:00:00_11:00:00"

post () { curl -s -X POST "$BASE_URL$1" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2"; echo; }

# 1. add
post /inventory/add_activity_time_slot_supplier_inventory "{
  \"activity_id\": $ACT, \"group_key\": \"$GK\",
  \"supplier_inventory\": [{ \"supplier_id\": 9, \"currency\": \"AED\",
    \"adult_purchase_price\": 100, \"total_quantity\": 20, \"priority\": 1 }] }"

# 2. read back
post /inventory/get_activity_time_slot_supplier_inventory "{
  \"activity_id\": $ACT, \"group_key\": \"$GK\", \"limit\": 5 }"

# 3. reprice the group
post /inventory/update_activity_time_slot_supplier_inventory "{
  \"activity_id\": $ACT, \"group_key\": \"$GK\", \"supplier_id\": 9,
  \"adult_purchase_price\": 130 }"

# 4. deactivate
post /inventory/update_activity_time_slot_supplier_inventory_status "{
  \"activity_id\": $ACT, \"group_key\": \"$GK\", \"supplier_id\": 9, \"status\": 0 }"

# 5. confirm it is inactive
post /inventory/get_activity_time_slot_supplier_inventory "{
  \"activity_id\": $ACT, \"group_key\": \"$GK\", \"status\": 0, \"limit\": 5 }"
```

---

## Two things to know before building on this

**Nothing reads this table yet.** `/v1/get_activity_supplier_inventory_booking` and
`/xApi/get_activity_supplier_inventory_booking` still query `activity_supplier_inventory`
only. Sourcing a slot booking from these rates is a separate piece of work.

**Deleting a time slot leaves its supplier rows behind.** There is no FK, deliberately —
`createTimeSlot` replaces a block by DELETE + INSERT, so `time_slot.id` is not stable and a
FK would destroy the rates on every admin re-save. The orphans are inert; the migration file
carries a reviewed `SELECT` and `DELETE` for cleanup.

Offline regression suite (stubs the pool, touches no database):

```bash
node scripts/test_time_slot_supplier_inventory.js
```
