feat: расчёт стоимости платного тестирования/аренды GPU-серверов

Новая вкладка «Аренда» в конфигураторе (доступна для проектов с флагом
rental_enabled): расчёт по методичке "Регламент расчёта стоимости
платного тестирования и аренды GPU-серверов" — Разовый + Еженедельный
платёж по каждому компоненту (New/БУ чекбоксом), с аплифтом к Estimate.

Заодно: поддержка (support_code) в Base-вкладке теперь добавляется как
обычный LOT в спеку (SVC_{срок}y{уровень}_{платформа}) через
autocomplete-пикер вместо выпадающего списка — партномер собирается
тем же lot_name-based механизмом, что и для GPU/CPU/памяти, справочная
цена по регламенту техподдержки хранится в qt_settings.support_pricing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-22 13:34:07 +03:00
co-authored by Claude Sonnet 5
parent b48436de93
commit 7edb80e498
25 changed files with 1052 additions and 71 deletions
+64 -1
View File
@@ -132,7 +132,7 @@ Rules:
## Server-driven configurator settings (`qt_settings`)
QF reads four settings from `qt_settings` (MariaDB) and caches them in `local_qt_settings` (SQLite).
QF reads settings from `qt_settings` (MariaDB) and caches them in `local_qt_settings` (SQLite).
They are synced during every component sync. See `bible-local/server-contract-qt-settings.md` for the
full contract and JSON schemas.
@@ -142,6 +142,7 @@ full contract and JSON schemas.
| `tab_config` | Configurator tab structure, sections, singleSelect |
| `always_visible_tabs` | Which tabs are shown even when empty |
| `required_categories` | Per-config-type badge on tabs with unfilled required categories |
| `support_pricing` | Price data for the Base tab's support-level picker (see below) |
Rules:
- sync runs as part of the pricelist pull; failure is non-fatal (Warn log only);
@@ -150,6 +151,68 @@ Rules:
- `config_types[].categories` is an allowlist: a category absent from all types is shown everywhere;
- `qt_categories.name` and `qt_categories.name_ru` are not used by QF runtime; do not depend on them.
## Support as a BOM LOT
The Base tab's support-level picker adds/replaces a synthetic LOT in `cart` (e.g.
`SVC_3yB_HGX-H200`, quantity 1) exactly like adding any other component — not a separate
mechanism. This means support flows through `total_price`, the Pricing tab, exports, and the
rental calc the same way any BOM line does, with no special-casing needed in those paths.
- lot_name shape: `SVC_{years}y{level}_{platform}` (e.g. `SVC_3yB_HGX-H200`); `internal/article
/generator.go`'s `buildSupportSegment` detects it by the `SVC_` lot_name prefix (support LOTs
aren't in the pricelist catalog, so there's no `lot_category` to key off — same lot_name-pattern
approach the generator already uses for GPU/CPU/memory parsing) and emits the `{years}y{level}`
token as the article's SUPPORT segment, replacing the old separate `BuildOptions.SupportCode`
field/`isSupportCodeValid` check;
- `Configuration.SupportCode` (the DB column) is no longer read by article generation; it's
inert legacy metadata now — the LOT in `items` is the source of truth;
- the picker's displayed price is computed client-side per "Регламент расчёта стоимости
технической поддержки серверов": x86 is a percent of the rest of the cart's total (proxy for
"цена продажи"), HGX platforms use a fixed multi-year price by chip generation. The pricing
table itself lives in `qt_settings["support_pricing"]` so it can be edited in MariaDB without
a QuoteForge release — see `bible-local/server-contract-qt-settings.md` for the schema; the
fixed list of offerable level×duration codes stays in the frontend, only the price is
server-driven;
- platform (x86 vs HGX-H100/H200 vs HGX-B200 vs HGX-B300) is auto-detected client-side from the
cart's GPU components, the same chip-generation classification used by the rental
depreciation calc (`internal/services/rental.go`), so the picker only offers combinations
valid for the current configuration;
- the support LOT has no `lot_category` (not in the pricelist), so it renders under the
"Other" category tab like any other uncategorized item — no new tab/category was added for it.
## Rental / paid-testing pricing contract
QuoteForge can quote paid testing / short-to-mid-term rental of a configuration's hardware, per the draft
regulation "Регламент расчёта стоимости платного тестирования и аренды GPU-серверов" (methodology may still
change; buyout/Step 9 of the regulation is intentionally not implemented).
Rules:
- rental is enabled per **project** via `Project.RentalEnabled` (`qt_projects.rental_enabled`); when set, the
configurator shows a 4th top-level tab ("Аренда") for every configuration in that project;
- New/БУ condition per component lives only on the configuration, in `Configuration.RentalItems`
(`RentalItemCondition{LotName, Condition}`) — it is not a property of the lot/pricelist and does not touch
`ConfigItem`/BOM; the same lot_name can be "new" in one configuration and "used" in another;
- the "Цена" the methodology depreciates against is `Estimate buy price × (1 + RentalUpliftPercent/100)` —
QuoteForge has no per-component sale price, so a single project-configuration-wide uplift percentage stands
in for it;
- pricing is always quoted per week — Разовый (one-time layer-1 hit) + Еженедельный (recurring); there is no
term/weeks input, a sales rep multiplies the weekly rate by however many weeks are needed;
- annual BASE support price is **computed automatically**, never entered manually, per "Регламент расчёта
стоимости технической поддержки серверов": any GPU component present classifies the configuration as HGX and
uses the regulation's fixed per-platform price (H100/H200, B200, B300, by lot_name substring); no GPU present
falls back to 5% of the uplifted price sum as an x86 sale-price proxy — `support_code` is pure
article-formatting metadata (see `internal/article/generator.go`) and is never resolved to a price, so there
is no SVC_-catalog lookup to reuse;
- GPU "actual vs stabilized generation" classification for depreciation life (2yr vs 3yr) and the separate
GPU "support platform" classification (H100/H200 vs B200 vs B300) are both hardcoded lot_name substring
lists in `internal/services/rental.go`, not sourced from `lot_category`;
- `RentalItems` and `RentalUpliftPercent` are included in the revision dedup fingerprint
(`BuildConfigurationSpecPriceFingerprint`), so rental edits create new revisions like other spec/price-affecting
changes;
- `internal/services/rental.go` (`RentalService.Calculate`) is stateless: it re-derives categories via
`GetLocalLotCategoriesByServerPricelistID` and computes pricing (including the auto support price) from the
request body, so calculate calls do not require saving first.
## Vendor BOM contract
Vendor BOM is stored in `vendor_spec` on the configuration row.
+3
View File
@@ -192,6 +192,8 @@ PK: lot_name
| line_no | int | position within project |
| price_updated_at | timestamp | |
| vendor_spec | longtext JSON | |
| rental_items | JSON | per-lot New/БУ condition for the Аренда tab; see [02-architecture.md](02-architecture.md#rental--paid-testing-pricing-contract) |
| rental_uplift_percent | decimal(8,2) DEFAULT 0 | uplift applied to Estimate buy price for rental "Цена" |
### qt_lot_metadata
PK: lot_name
@@ -294,6 +296,7 @@ PK: username
| tracker_url | varchar(500) | |
| is_active | tinyint(1) DEFAULT 1 | |
| is_system | tinyint(1) DEFAULT 0 | |
| rental_enabled | tinyint(1) DEFAULT 0 | shows the Аренда tab on this project's configurations |
| created_at | timestamp | |
| updated_at | timestamp ON UPDATE | |
+2
View File
@@ -80,6 +80,8 @@
| `PUT` | `/api/configs/:uuid/vendor-spec` | replace vendor BOM |
| `POST` | `/api/configs/:uuid/vendor-spec/resolve` | resolve PN -> LOT |
| `POST` | `/api/configs/:uuid/vendor-spec/apply` | apply BOM to cart |
| `PUT` | `/api/configs/:uuid/rental` | persist Аренда tab state (per-lot New/БУ, weeks, uplift %, annual support) |
| `POST` | `/api/configs/:uuid/rental/calculate` | compute rental/paid-testing pricing without persisting |
## Projects
@@ -148,6 +148,55 @@ badge on the tab label when required categories are missing.
---
### `support_pricing`
Price data from "Регламент расчёта стоимости технической поддержки серверов", used by the
Base tab's support-level picker to display a computed price next to the chosen support code.
This is reference/quoting data only — QF never adds it as a cart line or configuration cost;
see [02-architecture.md](02-architecture.md#support-pricing-reference-not-a-cart-item).
x86 is a percent of sale price (QF approximates sale price as the configuration's Estimate
total). HGX platforms (any GPU category component present, classified by chip generation via
lot_name substring) use a fixed multi-year price instead. Both are keyed `level -> duration in
years (as a JSON string key) -> value`; a combination absent from the table is not offered by
the regulation and the picker won't show it.
**Value format:**
```json
{
"x86_percent": {
"W": {"3": 0.06},
"B": {"1": 0.05, "3": 0.10, "5": 0.14},
"S": {"1": 0.07, "3": 0.13, "5": 0.18},
"P": {"1": 0.12, "3": 0.20, "5": 0.45}
},
"hgx_price": {
"HGX-H200": {
"B": {"1": 20000, "3": 55000, "5": 80000},
"S": {"1": 28000, "3": 70000, "5": 100000},
"P": {"1": 63000, "3": 105000}
},
"HGX-B200": {
"B": {"1": 40000, "3": 100000, "5": 145000},
"S": {"1": 55000, "3": 130000, "5": 185000},
"P": {"1": 101000, "3": 176000}
},
"HGX-B300": {
"B": {"1": 40000, "3": 100000, "5": 145000},
"S": {"1": 55000, "3": 130000, "5": 185000},
"P": {"1": 112000, "3": 187000}
}
}
}
```
Editing this setting in MariaDB (no QF release needed) changes the price shown by the picker
immediately on the next component sync — this is the intended way to keep support pricing
current as the regulation evolves.
---
## Backward compatibility
- If `qt_settings` does not exist (old server): QF logs `Warn` during sync and