Публичный 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.

Метод + путьBCScopeИсточник
GET /v1/whoamicabinet/apikeyswhoami (sentinel)backend/internal/core/cabinet/apikeys/di.go
GET /v1/productscatalog/canonicalproducts:readbackend/internal/core/catalog/canonical/api/public-http/handler.go
GET /v1/products/{id_or_viewable}catalog/canonicalproducts:readsame
POST /v1/products/resolvecatalog/canonicalproducts:readsame
POST /v1/search/proposalssearch/proposalsearch: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}/cancelsearch/proposalsearch:readbackend/internal/core/search/proposal/infra/http/estimate_job.go
GET /v1/catalog/categoriescatalog/canonicalproducts:readbackend/internal/core/catalog/canonical/api/public-http/reference_data.go
GET /v1/catalog/categories/{category_id}/analog-criteriacatalog/canonicalproducts:readbackend/internal/core/catalog/canonical/api/public-http/handler.go
GET /v1/catalog/characteristics, GET /v1/catalog/characteristics/{code}/valuescatalog/canonicalproducts:readbackend/internal/core/catalog/canonical/api/public-http/reference_data.go
POST /v1/products/searchcatalog/canonicalproducts:readbackend/internal/core/catalog/canonical/api/public-http/handler.go
GET /v1/canonical/{id}/analogscatalog/canonicalproducts:readbackend/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) + количество.

Рабочий порядок:

  1. Проверить API-ключ: GET /v1/whoami.
  2. Если оператор уточняет вид изделия и параметры, сначала получить активную canonical v2 категорию, её analog-criteria, коды и значения характеристик. analog-criteria содержит только рабочие оси контракта, а не все поля каталога. Затем сформировать строгий пул через POST /v1/products/search; текст, категория и требования в этом запросе объединяются логическим «И». Выбранную карточку прочитать через GET /v1/products/{id_or_viewable}.
  3. Если на руках supplier SKU, сопоставить строки через POST /v1/products/resolve.
  4. Для асинхронной обработки сметы отправить строки и их технические ограничения в 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.
  5. В каждом ответе задачи обновить строки по 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-проверкой.

Статус документа

Область действия

Входит:

  • HTTP/JSON endpoints.
  • RPC-сервисы через Connect/gRPC/gRPC-Web.
  • WebSocket подписки и push-обновления.
  • Auth middleware, visibility application и freshness metadata.

Не входит:

  • Сама бизнес-логика каталога, pricing, search и estimate.
  • Административные endpoints.

Публичный контракт

Вход

Выход

  • Вызовы во внутренние сервисы 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 varDefaultОписание
PUBLIC_API_HTTP_PORT8080listener для REST/RPC
PUBLIC_API_WS_PATH/ws/v1путь WS endpoint
PUBLIC_API_AUTH_OIDC_ISSUEROIDC issuer
PUBLIC_API_WS_MAX_CONNECTIONS_PER_CUSTOMER10лимит соединений

Локальный запуск

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.

По умолчанию утилита проходит полный клиентский маршрут:

  1. GET /v1/whoami — проверяет ключ и скоупы.
  2. POST /v1/products/resolve — отдельно показывает, как позиции входной сметы резолвятся в товары.
  3. POST /v1/estimates/proposals — подбирает предложения по списку позиций.
  4. 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/whoami unit + 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_active
  • public_api_ws_events_dropped_total{reason}

Сейчас observability ограничена базовыми health/readiness и логами локального контейнера.

Открытые вопросы / TODO

  • Добавить performance-бенчмарки по протоколам.
  • Определить mTLS-стратегию для B2B gRPC.
  • Автоматизировать генерацию контрактных схем из кода или наоборот.

Связанные документы