Публичный API
NOTE
Статус: Mixed — часть контракта уже live (см. секцию «Live coverage»), остальное остаётся target. Документ описывает целевую сервисную границу и одновременно фиксирует, какие endpoint’ы реально wired на момент последнего обновления. Правила маркировки — в
50-processes/documentation-standard.md.
Этот README описывает сервисную границу public-api. Читайте его, когда нужно понять внешний контракт системы, какие endpoint’ы уже работают и как REST, RPC и WebSocket сходятся к одному набору бизнес-сценариев.
Live coverage (HEAD 6461537d, 2026-08-12)
Auth-fast-path через nginx auth_request /_internal/auth/api-key/verify
(ADR-0051) полностью wired. Все методы ниже валидируются через одно subrequest,
кэшируются 30 секунд и получают trusted X-Tr-* headers перед api-server.
| Метод + путь | BC | Scope | Источник |
|---|---|---|---|
GET /v1/whoami | cabinet/apikeys | whoami (sentinel) | backend/internal/core/cabinet/apikeys/di.go |
GET /v1/products | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/public-http/handler.go |
GET /v1/products/{id_or_viewable} | catalog/canonical | products:read | same |
POST /v1/products/resolve | catalog/canonical | products:read | same |
POST /v1/search/proposals | search/proposal | search:read (через RequireScopeOrLegacyJWT transition bridge) | backend/internal/core/search/proposal/infra/http/handler.go |
POST /v1/estimates/jobs, GET /v1/estimates/jobs/{id}, POST /v1/estimates/jobs/{id}/cancel | search/proposal | search:read | backend/internal/core/search/proposal/infra/http/estimate_job.go |
GET /v1/catalog/categories | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/public-http/reference_data.go |
GET /v1/catalog/categories/{category_id}/analog-criteria | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/public-http/handler.go |
GET /v1/catalog/characteristics, GET /v1/catalog/characteristics/{code}/values | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/public-http/reference_data.go |
POST /v1/products/search | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/public-http/handler.go |
GET /v1/canonical/{id}/analogs | catalog/canonical | products:read | backend/internal/core/catalog/canonical/api/http/analogs_handler.go |
In-process BearerMiddleware удалён в Public API RBAC Phase 2 — токен валидируется только в auth-service / verify-handler, остальные процессы знают об identity через trusted headers.
Остальные endpoint’ы OpenAPI-спеки остаются target: handler не зарегистрирован
либо scope ещё не выдан. Активация каждого такого метода требует handler и
перевода scope из plannedScopes в activeScopes
(backend/internal/core/cabinet/apikeys/domain/scope.go).
Методы каталога используют те же read-side и фильтры, что раздел canonical товаров в админке. Это не второй поисковый индекс и не отдельный механизм ранжирования аналогов.
Как пользоваться клиенту
Первый поддержанный сценарий — смета из supplier SKU или уже известных
Tracium product refs (TR-XXXXXXX / canonical UUID) + количество.
Рабочий порядок:
- Проверить API-ключ:
GET /v1/whoami. - Если оператор уточняет вид изделия и параметры, сначала получить активную
canonical v2 категорию, её
analog-criteria, коды и значения характеристик.analog-criteriaсодержит только рабочие оси контракта, а не все поля каталога. Затем сформировать строгий пул черезPOST /v1/products/search; текст, категория и требования в этом запросе объединяются логическим «И». Выбранную карточку прочитать черезGET /v1/products/{id_or_viewable}. - Если на руках supplier SKU, сопоставить строки через
POST /v1/products/resolve. - Для асинхронной обработки сметы отправить строки и их технические
ограничения в
POST /v1/estimates/jobs, затем получать изменения черезGET /v1/estimates/jobs/{id}?after={cursor}. СинхронныйPOST /v1/search/proposalsостаётся отдельным коммерческим расчётом. Для первого рабочего сценария по уже накопленной базе использоватьmode=reference: он читает cached-наблюдения и не требует customer supplier credentials.mode=liveсначала использует customer credentials, а если для поставщика их нет — падает обратно на system credentials, разрешённые дляcustomer_proposal. - В каждом ответе задачи обновить строки по
lines[].idи сохранить новыйcursorв той же транзакции. Наcompleted_with_gapsпоказать кандидатыanalogs[]пользователю, а не заменить исходный товар автоматически.
Детальный контракт асинхронного сценария, включая статусы, курсор, ошибки и примеры ответов, — в инструкции для 1С.
POST /v1/products/resolve сейчас live для exact selector
supplier + supplier_sku и для flexible selector по manufacturer, mpn,
query, characteristics[]. Flexible resolver запрашивает до 300 SQL-кандидатов
для Go-scoring и возвращает клиенту до 10 ранжированных кандидатов. Нечёткий
подбор широких аналогов по похожим характеристикам зарезервирован отдельным
target endpoint’ом POST /v1/products/analog-search.
Подробный сценарий с curl-примерами: cookbook.md.
Для интеграции компонента подбора сметы в 1С: пошаговая инструкция,
Postman-сценарий и примеры ответов.
Назначение
Внешний API для клиентов системы: REST, Connect/gRPC/gRPC-Web и WebSocket поверх единых доменных сценариев и общего auth/freshness semantics.
Auth boundary зафиксирован в ADR-0051: пользовательский JWT-cookie обслуживает
cabinet/admin UI, а public system-to-system вызовы используют opaque API tokens
Authorization: Bearer trk_* со scope-проверкой.
Статус документа
- Тип знания:
mixed — partial live + target service boundary - Статус реализации: auth fast-path и read-side endpoint’ы из «Live coverage» wired через api-server; остальные endpoint’ы из OpenAPI-спеки остаются target
- Текущее место кода:
backend/cmd/api-server— основной runtime, регистрирует все live/v1/*через GroupRegistrar’ы соответствующих BCbackend/internal/core/cabinet/apikeys— auth-фундамент (api_keys storage, verify-handler, /v1/whoami)backend/internal/core/catalog/canonical/api/public-http— products read endpointsbackend/internal/core/search/proposal/infra/http— search proposalsbackend/internal/platform/apikeyauth—Subject+RequireScope+ trusted-headers parser
- Что читать дальше: интерактивный контракт — Swagger UI, исходники —
../../20-architecture/schemas/api/index.md,../../20-architecture/schemas/api/public-api.openapi.yaml,../../20-architecture/schemas/api/grpc/public_api.proto,../../20-architecture/adr/0015-multi-protocol-public-api.md,../../20-architecture/adr/0051-public-api-auth-and-api-key-rbac.md
Область действия
Входит:
- HTTP/JSON endpoints.
- RPC-сервисы через Connect/gRPC/gRPC-Web.
- WebSocket подписки и push-обновления.
- Auth middleware, visibility application и freshness metadata.
Не входит:
- Сама бизнес-логика каталога, pricing, search и estimate.
- Административные endpoints.
Публичный контракт
Вход
- REST: canonical external URLs live under
/v1/*:GET /v1/products,GET /v1/products/{id_or_viewable},POST /v1/products/resolve,POST /v1/search/proposals,POST /v1/pricing,POST /v1/estimatesи связанные endpoints. В OpenAPI path keys omit/v1, потому чтоservers.urlуже содержит base path/v1. - RPC: сервисы из
../../20-architecture/schemas/api/grpc/public_api.proto. - WebSocket: подписочная модель из
../../20-architecture/schemas/api/asyncapi/public-ws.yaml.
Выход
- Вызовы во внутренние сервисы
catalog-core,search,pricing,meta-search,enrichment,visibility. - Подписки на Kafka-топики для realtime-обновлений в WS.
- Постановка enrichment/discovery jobs.
Внутренняя архитектура
В целевом состоянии это transport/API edge-слой с трёмя адаптерами доступа: REST, RPC и WS. Он не хранит доменную логику, а приводит внешний контракт к внутренним use case’ам и поддерживает единые semantics ошибок, freshness и авторизации.
Зависимости
- Все ядерные сервисы и visibility.
- Kafka для WS подписок.
- Redis для rate limit и кэшей, если потребуется.
- OIDC/auth subsystem.
Хранилище
- Собственного основного domain-storage не предполагает.
- Может использовать Redis и служебные persistence-структуры для WS/subscription management.
Конфигурация
| Env var | Default | Описание |
|---|---|---|
PUBLIC_API_HTTP_PORT | 8080 | listener для REST/RPC |
PUBLIC_API_WS_PATH | /ws/v1 | путь WS endpoint |
PUBLIC_API_AUTH_OIDC_ISSUER | — | OIDC issuer |
PUBLIC_API_WS_MAX_CONNECTIONS_PER_CUSTOMER | 10 | лимит соединений |
Локальный запуск
Web-контур со всем nginx + auth-service + api-server поднимается командой:
make local-prod-upДоступен по https://api.tracium.dev:4444. Live-coverage endpoint’ы можно проверить напрямую (требуется выпустить API-ключ через /api/cabinet/keys после JWT-cookie логина):
curl -H "Authorization: Bearer trk_live_..." https://api.tracium.dev:4444/v1/whoami
curl -H "Authorization: Bearer trk_live_..." https://api.tracium.dev:4444/v1/productsДля последовательной отладки сценария сметчика используйте клиентскую
консольную утилиту. Она принимает любой заранее подготовленный
estimate-request.v1 JSON, вызывает реальные публичные endpoint’ы и печатает
в stdout структурированные JSON-события request/response.
По умолчанию утилита проходит полный клиентский маршрут:
GET /v1/whoami— проверяет ключ и скоупы.POST /v1/products/resolve— отдельно показывает, как позиции входной сметы резолвятся в товары.POST /v1/estimates/proposals— подбирает предложения по списку позиций.GET /v1/products/{id_or_viewable}— загружает карточки выбранных товаров и первых кандидатов, чтобы в логе были описания, характеристики, цены, остатки, условия доставки и supplier offers.
POST /v1/estimates/proposals закрывает строку сметы только коммерчески
пригодным proposal: в нём должна быть цена и подтверждённый
stock.status=ok с stock.available_qty >= qty_requested. Предложения из
основного pipeline, cached fallback и live-подбор аналогов, которые не
покрывают запрошенное количество или имеют stock.status=rejected, не
считаются закрывающими строку; строка остаётся needs_review или добирает
пригодный cached/analog fallback.
Цены из observation/read-model дополнительно проходят category sanity gate:
единичные экстремальные выбросы относительно p99 категории не используются как
коммерческая цена. Если другой валидной цены нет, строка остаётся
needs_review с proposal_not_found. Расчёт p99 является best-effort:
использует ограниченную выборку последних category offers и короткий
внутренний time budget, поэтому большая категория не блокирует синхронный
расчёт сметы.
Семантические аналоги в ответе сметы также фильтруются по минимальному score:
низкоскоринговые хвостовые кандидаты не показываются как analogs, даже если
у них есть цена и остаток. Это защищает сметчика от нерелевантных замен.
Если у товара есть несколько pole-count характеристик, ранжирование использует
total_number_of_poles / «общее количество полюсов» выше
protected_pole_count / «количество защищённых полюсов». Поэтому, например,
АВДТ 2P с одним защищённым полюсом не проходит как чистый аналог для запроса
на 1P автоматический выключатель.
Если у anchor-товара ещё нет embedding и строгий category-filter не дал
кандидатов, no-embedding analog ranker может расширить lookup за пределы
категории, но только при полном совпадении hard identity-характеристик
(rated_current, total_number_of_poles, tripping_characteristic и т.п.).
Такой fallback защищает смету от ошибочных taxonomy assignment, не превращая
подбор аналогов в общий text search.
Для автоматических выключателей смета также использует кА/kA из
raw_text, названий и характеристик как монотонный gate: кандидат с меньшей
отключающей способностью не поднимается выше подходящего аналога с равной или
большей способностью.
Когда контракт категории отверг ВСЕХ кандидатов, GET /v1/canonical/{id}/analogs
отдаёт блок partial_results вместо пустоты: лучшие по баллу из отвергнутых с
причиной отказа и перечнем расхождений по осям (differences). Блок отдельный —
к results частичные не подмешиваются, и контракт не ослабляется: отвергнутый
кандидат остаётся отвергнутым, решение принимает человек. Появляется он только
при пустом results. Отсев по качеству (кандидат без структурного сходства) в
блок не попадает — иначе он стал бы текстовым шумом. Потолок блока — рычаг
ANALOGS_PARTIAL_LIMIT (по умолчанию 5; отрицательное значение гасит блок).
Публичный direct analog endpoint GET /v1/canonical/{id}/analogs использует
те же safety defaults для автоматических выключателей. Если у anchor-товара в
фактах или названии есть breaking_capacity /
rated_short_circuit_breaking_capacity / icu / ics / 6кА, а category
rules не задали явное правило, кандидат с меньшим численным значением из факта
или названия отклоняется как under-spec. Если фактов ещё нет, direct ranker
также извлекает из breaker-like названий rated_current (16А/16A/C16),
poles (1P, однополюсный) и trip_curve (B/C/D/K) и отбрасывает
явные несовпадения. Это закрывает supplier-native категории, где seed правил
может быть уже, чем фактическая номенклатура поставщика.
Для DC-автоматов direct analog ranker дополнительно выводит safety fact
current_type_dc_compatible из current_type / voltage_type и названий
товаров (DC, «постоянный ток»). Hard gate включается только для DC-only
anchor; AC/DC остаётся структурным сигналом, но не отбрасывает AC-only
кандидатов. Для DC-only anchor кандидат без DC-совместимости или без
подтверждения DC отклоняется до финального display limit, поэтому AC-only
автомат не проходит как аналог DC anchor только за счёт совпадения тока,
полюсов и кривой.
До финального display-limit смета overfetch’ит небольшой bounded window
ranker-кандидатов, сначала проверяет их через cached product-offer enrichment,
поднимает аналоги с валидной ценой и достаточным остатком выше no-proposal
кандидатов, а дорогой live commercial proposal pass запускает только для
видимых аналогов без уже найденного proposal. Поэтому пригодная замена ниже
первых пяти ranker-результатов не теряется до проверки цены и склада, а
live-режим не тратит бюджет на скрытые или глубокие overfetch-кандидаты.
Сильные resolve-кандидаты используются как быстрый fallback, но короткий
resolve-fallback не блокирует analog ranker: ranker добирает overfetch-хвост,
после чего hard gates по току, полюсам и кА применяются к объединённому
пулу кандидатов. Эти же hard gates повторно применяются перед финальным
ответом по уже заполненным display fields аналогов, чтобы кандидат с явным
конфликтом вроде 4.5кА для запроса 6кА не прошёл только из-за валидной
цены и остатка. Ток для автоматов извлекается из явных форм 16А/16A и
компактных curve-current форм вроде C16; артикулы/MPN не склеиваются с
последующим брендом в ложные значения тока. Нормализованные числовые
характеристики из карточек товаров читаются из value_canonical /
value.amount; для отключающей способности raw_unit=кА нормализуется в
амперы перед сравнением.
TRACIUM_API_KEY=trk_live_... \
python3 scripts/public_api_estimate_e2e.py \
--input /path/to/estimate-request.json \
--base-url https://api.tracium.dev:4444 \
--insecure \
--llm-assisted \
--output-log /tmp/tracium-estimate-trace.jsonlЗагрузку карточек можно ограничить, чтобы не получить слишком большой лог:
python3 scripts/public_api_estimate_e2e.py \
--input /path/to/estimate-request.json \
--details-candidate-limit 2 \
--details-limit 30 \
--output-log /tmp/tracium-estimate-trace.jsonlДля быстрой проверки можно ограничить вход:
python3 scripts/public_api_estimate_e2e.py \
--input /path/to/estimate-request.json \
--line-limit 3 \
--resolve-limit 3Для production-регрессий по сметам и analog quality есть фиксированная
quality matrix. Скрипт создаёт временного production customer/key, вызывает
/v1/products/resolve, /v1/estimates/proposals, product details и direct
canonical analog probes, затем отзывает ключ и архивирует профиль. Он проверяет,
что предложения имеют fixed price и stock.status=ok с достаточным
available_qty, direct DC probes не возвращают non-DC аналоги, а direct 6кА
breaker probes не возвращают 4.5кА under-spec аналоги.
TRACIUM_API_KEY= CABINET_BASE=https://api.tracium.ru \
python3 scripts/public_api_estimate_quality_matrix.py --timeout 180Для быстрого smoke без product detail карточек:
TRACIUM_API_KEY= CABINET_BASE=https://api.tracium.ru \
python3 scripts/public_api_estimate_quality_matrix.py --timeout 180 --skip-detailsЕсли утилита запускается внутри docker-compose сети и обращается напрямую к
api-server, а не через nginx/auth-service, можно включить локальные trusted
headers:
docker run --rm --network tracium_network \
-v "$PWD:/workspace" -w /workspace python:3.12-alpine \
python scripts/public_api_estimate_e2e.py \
--input /workspace/tmp/estimate-request.json \
--base-url http://api:8080 \
--local-trusted \
--line-limit 10 \
--details-candidate-limit 2 \
--details-limit 10 \
--jsonlЭтот режим нужен только для локальной диагностики прямого api-server; внешний
клиент должен использовать обычный Authorization: Bearer trk_live_....
Диагностика latency resolve
Если POST /v1/products/resolve или POST /v1/estimates/proposals начинает
упираться в 504 на строках вроде “автоматический выключатель C16”, сначала
проверьте активные запросы Postgres. Для flexible resolver корректный план:
специфичные токены (C16, модель, артикул) идут через
supplier_offers_name_trgm_idx и bounded materialized candidate set; общие
контекстные слова (автоматический, выключатель) не должны попадать в SQL
вместе со специфичным токеном. Контекст учитывается в Go-scoring после
overfetch. Индекс supplier_offers_name_trgm_idx создаётся миграцией 0227.
Endpoint’ы, оставшиеся в target-зоне OpenAPI-спеки, возвращают 404 (handler не зарегистрирован) или отвергаются на этапе issuing key через D9 active-vs-planned гейт.
Тестирование
- Live endpoint’ы покрыты:
backend/internal/core/cabinet/apikeys/api/http/v1/whoami_handler_test.go—/v1/whoamiunit + integration.backend/internal/core/catalog/canonical/api/public-http/handler_test.go— products read и resolver.backend/internal/core/search/proposal/infra/http/handler_test.go— search proposals.backend/internal/platform/apikeyauth/require_scope_test.go+legacy_jwt_fallback_test.go— scope-проверки.backend/internal/core/cabinet/apikeys/api/http/verify/...— verify-handler integration (nginx subrequest contract).
- Контрактная проверка spec ↔ код:
make backend-openapilint-public(на 2026-05-09 — 70 MissingInCode by design — это target-spec; alarm только при появлении новых drift’ов). - Для целевых endpoint’ов нужны unit-тесты handlers/adapters, integration-тесты auth/freshness semantics и контрактная проверка OpenAPI/proto/AsyncAPI.
Наблюдаемость
В целевом состоянии сервис должен публиковать метрики:
public_api_requests_total{protocol,endpoint,status}public_api_request_duration_seconds{protocol,endpoint}public_api_ws_connections_activepublic_api_ws_events_dropped_total{reason}
Сейчас observability ограничена базовыми health/readiness и логами локального контейнера.
Открытые вопросы / TODO
- Добавить performance-бенчмарки по протоколам.
- Определить mTLS-стратегию для B2B gRPC.
- Автоматизировать генерацию контрактных схем из кода или наоборот.