- 400 → 422 для всех ошибок валидации входных данных (handlers: export, quote, sync, vendor_spec, partnumber_books, pricelist) - SQL-запросы вынесены из handlers в localdb (partnumber_books, pricelist, support_bundle); ValidateMariaDBConnection перенесён в internal/db/validate.go - List-ответы унифицированы: ключ items, поля total_count/page/per_page/total_pages (component, pricelist, partnumber_books); шаблоны обновлены - Молчаливые ошибки заменены на slog.Warn/Error (support_bundle, vendor_spec, component, configuration, local_configuration, localdb) - N+1 запросы устранены: batch-запросы в export.go и vendor_workspace_import.go - fmt.Println → slog в cmd/ (qfs, migrate, migrate_ops_projects, migrate_project_updated_at) - Заголовки recovery/verify добавлены во все 28 SQL-миграций - Добавлены bible-local/runtime-flows.md и bible-local/decisions/ - Обновлён субмодуль bible до v0.2.0-13 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
564 lines
17 KiB
Markdown
564 lines
17 KiB
Markdown
# 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}'
|
||
```
|