# 10 - Agent API Guide: Pricing Servers from a TZ

This guide is written for an AI agent that needs to price a server configuration
(техническое задание, ТЗ) using the QuoteForge HTTP API.

## Runtime assumptions

- QuoteForge runs locally, binds to `127.0.0.1:8080` by default.
- No authentication is required — the app is single-user, loopback-only.
- All responses are JSON. All request bodies are JSON unless stated otherwise.
- The port can be overridden with the `QF_SERVER_PORT` environment variable.

Base URL for all examples: `http://127.0.0.1:8080`

---

## Configuration composition rules

These rules are mandatory and must be respected before saving any configuration.

### 1. Every configuration must belong to a project

Configurations cannot be created in isolation. The correct sequence is:

1. Create a project (`POST /api/projects`) and save the returned `uuid`.
2. Create the configuration inside that project by passing `project_uuid` in the
   config body, or by using `POST /api/projects/:uuid/configs`.

If the project for a given TZ already exists, retrieve its `uuid` first:
```
GET /api/projects?page=1&per_page=100
```
then pass the matching `uuid` in `project_uuid`.

### 2. Every server configuration must contain all four required component groups

A configuration is not valid for pricing unless items from all four of the
following category groups are present:

| Category code | Meaning          | Notes                                             |
|---------------|------------------|---------------------------------------------------|
| `MB`          | Motherboard      | exactly one MB per configuration                  |
| `CPU`         | Processor        | one or more CPUs                                  |
| `MEM`         | Memory / RAM     | one or more memory modules                        |
| `PS` / `PSU`  | Power supply     | `PSU` is the current code; `PS` is legacy — both are accepted |

Before saving, verify the assembled BOM with `POST /api/quote/validate`:
the response `errors` array will contain `"Component not found: …"` entries
for unknown lot names, and `warnings` will list lots without a price.
Reject the configuration and report back to the user if any of the four
required categories is missing.

### 3. Category codes to use when searching

Use `category=<code>` in `GET /api/components` to narrow results:

```
GET /api/components?category=MB&search=X13&has_price=true
GET /api/components?category=CPU&search=Xeon+Gold&has_price=true
GET /api/components?category=MEM&search=32GB+DDR5&has_price=true
GET /api/components?category=PSU&search=800W&has_price=true
```

Retrieve the full list of active categories at any time:
```
GET /api/categories
```

---

## Typical workflow for pricing a server

```
1.  Check the app is up                 GET  /api/ping
2.  Find or create a project            GET  /api/projects  →  POST /api/projects
3.  Find the latest pricelist           GET  /api/pricelists/latest?source=estimate
4.  Look up lot names for MB            GET  /api/components?category=MB&search=…
5.  Look up lot names for CPU           GET  /api/components?category=CPU&search=…
6.  Look up lot names for MEM           GET  /api/components?category=MEM&search=…
7.  Look up lot names for PSU           GET  /api/components?category=PSU&search=…
8.  (Repeat for other components)       GET  /api/components?category=…&search=…
9.  Validate and calculate the quote    POST /api/quote/validate
10. (Optional) Compare price tiers      POST /api/quote/price-levels
11. Save configuration in the project   POST /api/projects/:uuid/configs
```

---

## Step 1 — Verify the app is running

```
GET /api/ping
```

Response `200 OK`:
```json
{"status": "ok"}
```

---

## Step 2 — Find or create a project

Each TZ maps to one project. Use the TZ identifier as the `code` field.

### Find an existing project

```
GET /api/projects?page=1&per_page=100
```

Response `200 OK`:
```json
{
  "projects": [
    {
      "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "code": "TZ-123",
      "variant": "",
      "name": "Проект по ТЗ №123",
      "tracker_url": "",
      "is_active": true,
      "created_at": "2026-06-01T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 100
}
```

### Create a new project

```
POST /api/projects
Content-Type: application/json
```

Request body:
```json
{
  "code": "TZ-123",
  "name": "Проект по ТЗ №123",
  "tracker_url": ""
}
```

Fields:

| field         | type   | required | description                                                        |
|---------------|--------|----------|--------------------------------------------------------------------|
| `code`        | string | yes      | short identifier, unique per variant; use the TZ number or ticket  |
| `variant`     | string | no       | variant label within the same `code`; default is empty string      |
| `name`        | string | no       | human-readable title                                               |
| `tracker_url` | string | no       | link to a ticket or issue tracker                                  |

Response `201 Created`:
```json
{
  "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "code": "TZ-123",
  "variant": "",
  "name": "Проект по ТЗ №123",
  "is_active": true,
  "created_at": "2026-06-11T10:00:00Z"
}
```

Save the `uuid` — it is required to create configurations inside this project.

---

## Step 3 — Find the latest pricelist

QuoteForge maintains three pricing tiers. The `source` values are:

| source       | meaning                     |
|--------------|-----------------------------|
| `estimate`   | list / catalogue price      |
| `warehouse`  | stock price (purchase cost) |
| `competitor` | competitor reference price  |

```
GET /api/pricelists/latest?source=estimate
```

Response `200 OK`:
```json
{
  "id": 42,
  "source": "estimate",
  "version": "2026-05-28",
  "item_count": 12500,
  "is_active": true,
  "created_at": "2026-05-28T06:00:00Z"
}
```

The `id` field is a numeric pricelist identifier. Pass it as `pricelist_id`
when calculating a quote to pin pricing to a specific pricelist.

To list all available pricelists:
```
GET /api/pricelists?source=estimate&active_only=true
```

---

## Steps 4–8 — Look up component lot names

Each component is identified by a `lot_name` (internal SKU). The TZ typically
contains model names or descriptions; use the search endpoint to resolve them.

```
GET /api/components?search=Xeon+Gold+6342&category=CPU&has_price=true&page=1&per_page=20
```

Query parameters:

| parameter        | default | description                                       |
|------------------|---------|---------------------------------------------------|
| `search`         | —       | free-text search in lot name and description      |
| `category`       | —       | filter by category code (`MB`, `CPU`, `MEM`, `PSU`, …) |
| `has_price`      | false   | return only components that have a price          |
| `include_hidden` | false   | include hidden/retired components                 |
| `page`           | 1       | page number                                       |
| `per_page`       | 20      | page size                                         |

Response `200 OK`:
```json
{
  "components": [
    {
      "lot_name": "CPU-XEON-6342",
      "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz, LGA4189",
      "category": "CPU",
      "category_name": "CPU",
      "model": "Xeon Gold 6342"
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 20
}
```

To look up a single component by exact lot name:
```
GET /api/components/CPU-XEON-6342
```

To list all known categories:
```
GET /api/categories
```

---

## Step 9 — Validate and calculate the quote

Before saving, validate the assembled BOM. This catches unknown lot names and
missing prices, and also confirms that all required categories are covered.

```
POST /api/quote/validate
Content-Type: application/json
```

Request body:
```json
{
  "items": [
    {"lot_name": "MB-X13DAI-N",        "quantity": 1},
    {"lot_name": "CPU-XEON-6342",       "quantity": 2},
    {"lot_name": "RAM-32GB-DDR4-3200",  "quantity": 8},
    {"lot_name": "SSD-480GB-SATA",      "quantity": 2},
    {"lot_name": "PSU-800W-TITANIUM",   "quantity": 2}
  ],
  "pricelist_id": 42
}
```

Response `200 OK`:
```json
{
  "valid": true,
  "items": [
    {
      "lot_name": "MB-X13DAI-N",
      "quantity": 1,
      "unit_price": 95000.00,
      "total_price": 95000.00,
      "description": "Supermicro X13DAi-N dual-socket server board",
      "category": "MB",
      "has_price": true
    },
    {
      "lot_name": "CPU-XEON-6342",
      "quantity": 2,
      "unit_price": 87500.00,
      "total_price": 175000.00,
      "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz",
      "category": "CPU",
      "has_price": true
    },
    {
      "lot_name": "RAM-32GB-DDR4-3200",
      "quantity": 8,
      "unit_price": 12000.00,
      "total_price": 96000.00,
      "description": "32 GB DDR4-3200 ECC RDIMM",
      "category": "MEM",
      "has_price": true
    },
    {
      "lot_name": "PSU-800W-TITANIUM",
      "quantity": 2,
      "unit_price": 18500.00,
      "total_price": 37000.00,
      "description": "800W 80+ Titanium redundant PSU",
      "category": "PSU",
      "has_price": true
    }
  ],
  "errors": [],
  "warnings": [],
  "total": 403000.00
}
```

**Agent check after validation:**

1. `valid` must be `true` — all lot names resolved.
2. `errors` must be empty — no unknown components.
3. The returned `items` array must contain at least one entry from each required
   category: `MB`, `CPU`, `MEM`, and `PS` or `PSU`.
4. Items with `has_price: false` are allowed but should be flagged to the user.

If any check fails, do not save the configuration. Report the issue and ask the
user to clarify or replace the problematic component.

For simple price totals without validation metadata use `POST /api/quote/calculate`
— identical request body, response contains only `items` and `total`.

---

## Step 10 (optional) — Compare price tiers

To see estimate, warehouse, and competitor prices side-by-side for a BOM:

```
POST /api/quote/price-levels
Content-Type: application/json
```

Request body:
```json
{
  "items": [
    {"lot_name": "CPU-XEON-6342", "quantity": 2},
    {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8}
  ],
  "pricelist_ids": {
    "estimate": 42,
    "warehouse": 31,
    "competitor": 15
  },
  "no_cache": false
}
```

`pricelist_ids` is optional. When omitted the latest pricelist for each source
is used automatically.

Response `200 OK`:
```json
{
  "items": [
    {
      "lot_name": "CPU-XEON-6342",
      "quantity": 2,
      "estimate_price": 87500.00,
      "warehouse_price": 71000.00,
      "competitor_price": 85000.00,
      "delta_wh_estimate_abs": -16500.00,
      "delta_wh_estimate_pct": -18.86,
      "delta_comp_estimate_abs": -2500.00,
      "delta_comp_estimate_pct": -2.86,
      "delta_comp_wh_abs": 14000.00,
      "delta_comp_wh_pct": 19.72,
      "price_missing": []
    }
  ],
  "resolved_pricelist_ids": {
    "estimate": 42,
    "warehouse": 31,
    "competitor": 15
  }
}
```

`price_missing` lists the source names for which no price was found for that lot.
Delta fields are `null` when either operand price is missing.

---

## Step 11 — Save a configuration inside the project

Use the project-scoped endpoint so the configuration is immediately linked to
the correct project without a separate move operation.

```
POST /api/projects/:project_uuid/configs
Content-Type: application/json
```

The request body is identical to `POST /api/configs` — the `project_uuid` field
in the body is ignored when using the project-scoped route; the URL parameter
takes precedence.

Request body:
```json
{
  "name": "Сервер по ТЗ №123 — вариант А",
  "items": [
    {"lot_name": "MB-X13DAI-N",       "quantity": 1, "unit_price": 95000.00},
    {"lot_name": "CPU-XEON-6342",      "quantity": 2, "unit_price": 87500.00},
    {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8, "unit_price": 12000.00},
    {"lot_name": "SSD-480GB-SATA",     "quantity": 2, "unit_price": 8500.00},
    {"lot_name": "PSU-800W-TITANIUM",  "quantity": 2, "unit_price": 18500.00}
  ],
  "server_model": "2U",
  "support_code": "NBD",
  "server_count": 1,
  "pricelist_id": 42,
  "warehouse_pricelist_id": 31,
  "competitor_pricelist_id": 15,
  "config_type": "server",
  "notes": "Автоматически создано агентом на основании ТЗ №123"
}
```

Key fields:

| field                    | type   | required | description                                         |
|--------------------------|--------|----------|-----------------------------------------------------|
| `name`                   | string | yes      | human-readable name                                 |
| `items`                  | array  | yes      | `{lot_name, quantity, unit_price}` from validate    |
| `server_model`           | string | no       | chassis/form-factor code; used for article generation |
| `support_code`           | string | no       | support tier code; used for article generation      |
| `server_count`           | int    | no       | number of identical servers; total is multiplied    |
| `pricelist_id`           | uint   | no       | estimate pricelist to attach                        |
| `warehouse_pricelist_id` | uint   | no       | warehouse pricelist to attach                       |
| `competitor_pricelist_id`| uint   | no       | competitor pricelist to attach                      |
| `config_type`            | string | no       | `"server"` (default) or `"storage"`                 |
| `notes`                  | string | no       | free text                                           |
| `custom_price`           | float  | no       | override total price                                |
| `disable_price_refresh`  | bool   | no       | prevent automatic price refresh on open             |
| `only_in_stock`          | bool   | no       | filter to in-stock components only                  |

Response `201 Created`:
```json
{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Сервер по ТЗ №123 — вариант А",
  "items": [...],
  "total_price": 403000.00,
  "server_count": 1,
  "config_type": "server",
  "article": "2U-6342x2-32GBx8-NBD",
  "project_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "created_at": "2026-06-11T10:00:00Z"
}
```

The `uuid` can be used for all subsequent operations on this configuration.

---

## Working with saved configurations

```
GET  /api/configs/:uuid                 — retrieve a saved configuration
PUT  /api/configs/:uuid                 — full update (same body as create)
POST /api/configs/:uuid/refresh-prices  — re-price from latest pricelist
POST /api/configs/:uuid/clone           — duplicate: body {"name": "clone name"}
GET  /api/configs/:uuid/versions        — revision history
GET  /api/configs?page=1&per_page=20    — list all configurations
```

---

## Error responses

All error responses follow the same shape:

```json
{"error": "human-readable message"}
```

Common status codes:

| code | meaning                                               |
|------|-------------------------------------------------------|
| 400  | invalid request body or validation failure            |
| 404  | entity (component, pricelist, config) not found       |
| 423  | sync readiness is blocked; retry after sync completes |
| 500  | internal server error                                 |

---

## Minimal end-to-end example

```bash
BASE=http://127.0.0.1:8080

# 1. Verify the app is up
curl -s $BASE/api/ping

# 2. Create a project for this TZ
PROJECT_UUID=$(curl -s -X POST $BASE/api/projects \
  -H "Content-Type: application/json" \
  -d '{"code": "TZ-123", "name": "Проект по ТЗ №123"}' | jq -r .uuid)

# 3. Get latest estimate pricelist
PRICELIST_ID=$(curl -s "$BASE/api/pricelists/latest?source=estimate" | jq .id)

# 4. Find lot names for required categories
curl -s "$BASE/api/components?category=MB&search=X13&has_price=true"  | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=CPU&search=Xeon&has_price=true" | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=MEM&search=32GB&has_price=true" | jq '.components[].lot_name'
curl -s "$BASE/api/components?category=PSU&search=800W&has_price=true" | jq '.components[].lot_name'

# 5. Validate the BOM (must contain MB, CPU, MEM, PSU/PS)
curl -s -X POST $BASE/api/quote/validate \
  -H "Content-Type: application/json" \
  -d "{
    \"pricelist_id\": $PRICELIST_ID,
    \"items\": [
      {\"lot_name\": \"MB-X13DAI-N\",        \"quantity\": 1},
      {\"lot_name\": \"CPU-XEON-6342\",       \"quantity\": 2},
      {\"lot_name\": \"RAM-32GB-DDR4-3200\",  \"quantity\": 8},
      {\"lot_name\": \"PSU-800W-TITANIUM\",   \"quantity\": 2}
    ]
  }" | jq '{valid, errors, warnings, total}'

# 6. Save the configuration inside the project
curl -s -X POST "$BASE/api/projects/$PROJECT_UUID/configs" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Сервер по ТЗ №123\",
    \"pricelist_id\": $PRICELIST_ID,
    \"server_model\": \"2U\",
    \"server_count\": 1,
    \"config_type\": \"server\",
    \"items\": [
      {\"lot_name\": \"MB-X13DAI-N\",        \"quantity\": 1,  \"unit_price\": 95000},
      {\"lot_name\": \"CPU-XEON-6342\",       \"quantity\": 2,  \"unit_price\": 87500},
      {\"lot_name\": \"RAM-32GB-DDR4-3200\",  \"quantity\": 8,  \"unit_price\": 12000},
      {\"lot_name\": \"PSU-800W-TITANIUM\",   \"quantity\": 2,  \"unit_price\": 18500}
    ]
  }" | jq '{uuid, total_price, article}'
```
