- 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>
17 KiB
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:8080by 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_PORTenvironment 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:
- Create a project (
POST /api/projects) and save the returneduuid. - Create the configuration inside that project by passing
project_uuidin the config body, or by usingPOST /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:
{"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
validmust betrue— all lot names resolved.errorsmust be empty — no unknown components.- The returned
itemsarray must contain at least one entry from each required category:MB,CPU,MEM, andPSorPSU. - Items with
has_price: falseare 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:
{
"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:
{
"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:
{
"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:
{
"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:
{"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
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}'