Adding a new supplier

Спецификация приемки нового поставщика end-to-end. Ее цель - не просто добавить HTTP-клиент, а гарантировать, что ассортимент, цены, остатки по всем складам, характеристики, описания и downstream matching регулярно попадают в нашу базу без SKU-by-SKU узких мест.

Непереговорные правила

  • Credentials поставщиков живут в Credentials BC / базе данных. В .env разрешены только инфраструктурные настройки: base URL, лимиты, таймауты, режим enumerator, DSN, Redis/S3/LLM endpoints. Логины, пароли, токены, master key и клиентские доступы в .env не искать и не добавлять.
  • Для поставщика с batch, bulk, cursor, file-feed или incremental API запрещено строить регулярную загрузку через отдельный запрос на каждый SKU. Per-SKU запросы допустимы только как явно задокументированный fallback для поставщика без массового API или для точечной диагностики.
  • Остатки нужно собирать по всем складам, которые отдает поставщик. Если authoritative stock snapshot успешно загружен и в нем нет SKU, это означает загруженный пустой остаток, а не ошибку missing_remains.
  • Каждый поставщик обязан иметь mock/imitation service до merge. Mock должен воспроизводить не только happy path, но и лимиты, ошибки, paging, частичные данные и реальные странности payload.
  • Новый supplier считается готовым только после локальных contract/mock тестов, bounded live tick на реальных DB-backed credentials и downstream прогонов: charnorm-worker, matcher-worker, canonical-assignment-worker.

1. Разбор спецификации API

  • Описать transport: REST, SOAP, file-feed, SFTP, headless или гибрид.
  • Выделить оси данных:
    • catalog/goods: название, описание, бренд, категория, изображения, документы;
    • characteristics: сырые атрибуты, единицы измерения, группы;
    • prices: типы цен, валюта, НДС, минимальные партии, инкременты;
    • stock/remains: остатки по всем складам, коды складов, названия, резервы;
    • delivery/availability: сроки, кратность, ограничения поставки.
  • Зафиксировать ведущую ось snapshot: обычно catalog/goods. Price-only или stock-only SKU нельзя молча превращать в полноценный товар без описания.
  • Найти массовые стратегии:
    • full catalog snapshot;
    • batch prices by page/cursor/file;
    • stock all warehouses;
    • incremental changes by timestamp/cursor;
    • batch details by list of SKU, если полного snapshot нет.
  • Записать лимиты: page size, rate limit, timeout, daily quota, retry-after, ограничения по IP whitelist.
  • Сопоставить ошибки поставщика с ConnectorErrorCode: AuthRejected, SessionExpired, RateLimited, NotFound, OnRequest, Transient, Permanent, PartialSuccess.

2. Capabilities и стратегия загрузки

  • Добавить Capabilities для поставщика и явно описать:
    • поддерживается ли BulkSnapshotSource;
    • есть ли incremental/cursor режим;
    • есть ли batch lookup по списку SKU;
    • есть ли per-warehouse stock snapshot;
    • какие оси могут быть частичными.
  • Регулярный refresh должен использовать самую широкую доступную стратегию:
    1. incremental/cursor, если поставщик гарантирует полноту изменений;
    2. full/bulk snapshot по страницам или файлам;
    3. batch by SKU list;
    4. per-SKU только как последний fallback с отдельным лимитом и алертом.
  • Если Capabilities.ScheduledRefresh содержит batch/paged/incremental стратегию, supplier обязан иметь зарегистрированный bulk snapshot source. Router не должен молча падать в legacy per-SKU pipeline.
  • Для batch endpoint нельзя связывать данные по номеру страницы между разными endpoint. Join делается только через supplier SKU / commercialRef / артикул поставщика.
  • Late page error не должен обнулять уже загруженные страницы. Поток должен отдавать завершенные items и отдельно фиксировать ошибку поздней страницы.
  • Если ось stock загружена полностью, отсутствие SKU в stock snapshot трактуется как пустые остатки. Если stock endpoint упал, это partial с missing_remains.

3. Интеграция в backend

  • core/ingestion/infra/<supplier>/session.go - SessionManager, login, session cache, auth bucket.
  • core/ingestion/infra/<supplier>/bulk_snapshot_source.go или equivalent - массовая загрузка данных. Для batch-capable поставщика это основной путь.
  • core/ingestion/infra/<supplier>/connector.go - точечный fetch только если нужен fallback или diagnostic path.
  • core/ingestion/infra/<supplier>/enumerator_*.go - enumerator не должен заставлять основной refresh ходить по SKU по одному, если есть bulk API.
  • core/normalization/infra/<supplier>/ - mappers для goods, price, stock, characteristics и raw refs.
  • Category mapper (offer_mapper.go::categoriesFrom*) обязательно заполняет normdom.CategoryRef.ExternalRef raw supplier-native ID до санитизации slug’а (per-level). Resolver использует это как natural key. Name может быть пустым (fallback на Slug); ExternalRef пустым быть не может — иначе resolver вернёт CodeCategoryResolverInvalidRef.
  • core/ingestion/infra/<supplier>/capabilities.go и di.go - supplier module.
  • cmd/supplier-sync/main.go - зарегистрировать module и wiring.
  • ЛК клиента - добавить поставщика в клиентские сценарии подключения, отображение статуса credentials, health, errors и доступные scopes.
  • Kafka topics - если появились новые producer topics, добавить их в startup initialization для local и e2e compose.

4. Mock/imitation service

Mock обязателен и должен жить в deploy/docker/resources/mock-<supplier>/.

  • Сервис поднимается в docker compose profile test.
  • Фикстуры лежат рядом с mock или в backend/test/fixtures/<supplier>/.
  • Happy path включает несколько страниц catalog, price, stock и минимум два склада.
  • Проверяются реальные edge cases:
    • auth success/fail;
    • expired session;
    • rate limit и retry-after;
    • transient 5xx на поздней странице;
    • paging order mismatch между catalog и price;
    • numeric и string supplier SKU;
    • null/empty arrays;
    • SKU без stock row при полном stock snapshot;
    • SKU без price row;
    • partial axis response;
    • неизвестный склад;
    • конфликтующие характеристики;
    • provider-specific strange payload, найденный на live данных.
  • Если live проверка нашла новый формат payload, добавить sanitized fixture и regression test.

5. Тесты

  • Provider contract suite: backend/test/provider-contract/<supplier>_connector_contract_test.go.
  • Unit tests для session, request builder, error classifier и rate limit.
  • Mapper tests для goods, price, stock, characteristics:
    • numeric/string SKU;
    • пустые значения;
    • валюта, НДС, упаковка;
    • все склады;
    • raw refs сохраняются.
  • Bulk snapshot tests:
    • regular refresh не делает per-SKU запросы для batch-capable supplier;
    • batch-capable supplier без bulk source завершает tick явной ошибкой, а не legacy per-SKU fallback;
    • join между осями идет по SKU, а не по странице;
    • late page error сохраняет уже отданные items;
    • stock absence после полного snapshot дает empty stock payload;
    • provider pagination не теряет последнюю страницу.
  • Mock E2E: supplier-sync --tick-once против mock пишет supplier_offers и offer_observations.
  • Downstream tests или integration smoke: charnorm-worker, matcher-worker, canonical-assignment-worker.

6. Live verification

Основной runbook: ../40-operations/runbooks/live-supplier-contour.md.

  • Не искать provider credentials в .env.
  • Проверить активные system/customer credentials через БД или app repositories без вывода секретов.
  • Запустить live supplier tick на реальных данных. Если API отдает full-export, max_items должен быть 0 и обработка идет по всему export; положительный max_items допустим только для отдельного smoke/debug-прогона и не считается production acceptance.
  • После ingestion запустить:
    • charnorm-worker --batch-once;
    • matcher-worker --batch-once;
    • canonical-assignment-worker --batch-once.
  • DB assertions должны показать:
    • количество уникальных SKU;
    • goods payload count;
    • price payload count;
    • stock payload count;
    • non-empty prices;
    • non-empty stock by warehouses;
    • partial/error counts and missing kinds;
    • characteristic_raw rows;
    • charnorm mappings/backfills;
    • matcher decisions;
    • canonical assignments или assignment disputes.
  • После ручного one-shot вернуть постоянно работающий supplier-sync, если он был остановлен для проверки.
  • Для supplier overlap проверить не только количество canonical products, но и реальные группы схлопывания. Одинаковый физический товар у разных поставщиков должен иметь один canonical; товары разных производителей или брендов не схлопывать только из-за совпадения характеристик.

Минимальный формат отчета:

supplier:
credential scope: system/customer/both
command:
unique_sku:
goods_payload:
price_payload:
stock_payload:
nonempty_price:
nonempty_stock:
warehouses_seen:
partial:
errors:
raw_characteristics:
charnorm_mappings:
matcher_decisions:
canonical_assignments:
assignment_disputes:
canonical_overlap_groups:
gaps:

7. LLM, характеристики и конфликты

  • Сырые характеристики должны попасть в characteristic_raw.
  • Charnorm должен создать/обновить mappings и backfill normalized attrs.
  • Matcher должен создать decisions для новых offers.
  • Canonical assignment должен создать assignments либо disputes.
  • LLM resolver должен валидировать output:
    • confidence может приходить как 0.87 или 87;
    • selected values не должны сохраняться с лишними кавычками;
    • hallucinated/unknown values уходят в manual review или retryable state;
    • in_progress disputes не должны зависать навсегда.
  • Resolver нельзя проверять серией быстрых рестартов: один LLM batch должен либо завершиться summary, либо штатно вернуться через stale reclaim. Иначе тест сам создаёт временные in_progress.

8. Kafka topics

Все producer topics из production Go-кода должны создаваться при старте local и e2e compose. Перед merge выполнить сравнение:

prod_topics=$(mktemp); local_topics=$(mktemp); e2e_topics=$(mktemp)
rg -o '"[a-z0-9_.-]+\.v[0-9]+"' backend/internal --glob '*.go' --glob '!*_test.go' | sed 's/.*"\(.*\)"/\1/' | sort -u > "$prod_topics"
sed -n '/for topic in \\/,/; do/p' deploy/docker/docker-compose.yml | rg -o '[a-z0-9_.-]+\.v[0-9]+' | sort -u > "$local_topics"
sed -n '/for topic in \\/,/; do/p' deploy/docker/docker-compose.e2e.yml | rg -o '[a-z0-9_.-]+\.v[0-9]+' | sort -u > "$e2e_topics"
comm -23 "$prod_topics" "$local_topics"
comm -23 "$prod_topics" "$e2e_topics"
rm -f "$prod_topics" "$local_topics" "$e2e_topics"

Обе команды comm не должны вывести ни одной строки.

9. Documentation and runbook

  • docs/docs/30-services/ingestion/connectors/<supplier>.md - transport, capabilities, session, rate limits, error taxonomy, egress, mock service, live verification notes.
  • docs/docs/30-services/ingestion/supplier-sync-runbook.md - custom ops steps if supplier needs them.
  • ADR only if supplier requires behavioral deviation from ADR-0024 / ADR-0033.
  • Сохранить реализованные API contracts и sanitized payload fixtures, даже если полноценная интеграция поставщика переносится на будущее.

Definition of done

  • Batch/bulk/incremental стратегия выбрана и покрыта тестами.
  • Per-SKU регулярной загрузки нет для supplier с массовыми API.
  • Остатки собираются по всем складам.
  • Price и stock данные не теряются из-за разного порядка страниц.
  • Mock service покрывает happy path и edge cases.
  • Local mock E2E проходит.
  • Bounded live contour проверен на DB-backed credentials.
  • Charnorm, matcher, canonical assignment прошли после загрузки.
  • Canonical merge проверен на targeted overlap-наборе, включая multi-supplier совпадение и negative case разных брендов/производителей.
  • ЛК клиента умеет подключить поставщика и показать health/errors.
  • Kafka topics создаются при старте local/e2e compose.
  • Все regression tests и git diff --check проходят.

References

  • ADR-0024 supplier connector contract
  • ADR-0033 supplier imitation services mandated
  • docs/plans/2026-04-20-11-50-phase1a-ingestion-etm.md (ETM template)
  • docs/superpowers/specs/2026-04-20-ingestion-phase1-a-design.md