ADR-0059: ETM batch shape — per-store remains, per-50 prices, SgGds = identification only

Status: accepted Date: 2026-05-15 Deciders: Maxim Belkanov

Контекст

Initial ETM connector design (2026-04-23, до commit b31cb18) assumed SgGds async job (/api/v1/job/{uuid}) returned identification + commerce data per SKU и был основным источником наполнения. На практике это оказалось не так: на job-respose users получают только {id, name, brand, article, brand_code, cli_code, class_code, class} без цен и без остатков.

Параллельно BulkSnapshotSource.fetchWarehouseStockOnce вызывал batch /goods/remains БЕЗ обязательного параметра store=. Для текущего single-warehouse credential (warehouse_scope = {14} Москва) это работало — ETM auto-fills credential’s scope. Для multi-warehouse credential ETM «обрубает по объёму» JSON (spec ETM API §3.3) и часть SKU теряется.

Empirical curl verification (2026-05-15, prod ETM):

PathShapeVerdict
/goods/{ids%2C…id50}/price?type=etmbatch 50 OK, returns 50 rowsOK
/goods/{ids%2C…}/remains404 «Товар не найден»NOT SUPPORTED
/goods/{id}/remains?type=etmsingle SKU; full payload InfoStores+InfoForecast+InfoSuppStores+DeliveryTimeOK (single only)
/goods/remains?store=N1,N2до 2 store codes / call; flat rows {StoreCode, GdsCode, Article, RemInfo}, только positive remainsOK
/goods/remains?store=N1,N2,N3400NOT SUPPORTED
/goods/remains?store=N (где N не в правах credential)400 «Нет такого склада в правах»discovery signal

Решение

SgGds — identification only

enumerator_live.go остаётся в графе как catalog enumerator only: производит SkuIdentityPack (mapping our nomenclature → ETM SKU code). Commerce data (price/stock) — отдельные endpoints. Job больше не участвует в bulk-обновлении цен/остатков и не упомянут в commerce-tier roadmap.

Stock pipeline — per-store pair iteration

BulkSnapshotSource.fetchWarehouseStockOnce итерирует cred.WarehouseScope парами по 2 через /goods/remains?store=N1,N2. Empty WarehouseScope → one-time probe 11 documented store кодов (DefaultETMStores); successful subset persists back через Repository.SetWarehouseScope. Subsequent ticks читают persisted scope и skip probe.

Trade-off: batch dump возвращает только positive remains rows. SKU отсутствующий в response = 0 stock на этом складе (encoded connector’ом как emptyETMRemainsEnvelope так что canonical observation всё равно записывается). Full per-SKU shape (forecast, supplier warehouses, delivery time) недоступен из bulk-пути — используется только на customer-search hot path (live-refresh tier-0).

StoreCode → human name map в warehouse_catalog.go (11 documented кодов: СПБ/Урал/Самара/Москва/ЮГ/Сибирь/Казань/Чехов/Малоярославец/ Воронеж/Владивосток). Batch dump не возвращает StoreName, per-SKU возвращает — enrichment apply’ится только в bulk path.

Price pipeline — per-50 batch path-CSV

/goods/{ids%2C…id50}/price?type=etm batch до 50 ids; response row cap = 50 (ETM hard limit). Default SUPPLIER_ETM_PRICE_BATCH_SIZE снижен 200 → 50 чтобы env-default-effective совпадали. Existing etmPriceResponseRowCap = 50 internal cap остаётся как safety net на случай operator override.

Все 4 price fields ETM (price / pricewnds / price_tarif / price_retail) мапятся в PriceSet.{Net, Gross, ListTarif, RetailRec} через normalization/infra/etm/price_mapper.go (без изменений в этом цикле — уже было). Client (договорная) и retail (общая) сохраняются параллельно; downstream discount rules применяются от RetailRec.

Out of scope (отложено)

  • Multi-credential dispatcher service: shared-memory coordinator для load distribution по multiple system credentials. Каждый credential знает свой warehouse_scope; coordinator partition’ит работу так, чтобы все authorised stores покрылись ровно один раз без duplication, и SKU shard’ы прайсов раскладывались равномерно. Probable shape: Redis-backed claim table keyed by (supplier, store_code) и (supplier, sku_shard), lease TTL ≤ tick interval. Отдельный ADR-0060.
  • Per-credential rate buckets keyed by CredentialContext.CredentialRef в ingdom.RatePolicy. Сейчас shared policy.Data bucket общий для всех ETM calls вне зависимости от credential — блокирует честную параллелизацию multiple sessions. Парный ADR-0060.
  • Admin UI: warehouse_scope editor на credential detail page для override discovery результата.

Последствия

  • Initial probe для empty-scope credential стоит до 11 sequential calls (~11s @ 1 req/sec). Pay once per credential — persists.
  • Multi-warehouse credentials больше не теряют stock из-за JSON truncation.
  • ETM rate budget per credential предсказуем: ⌈|stores|/2⌉ stock calls
    • ⌈|SKUs|/50⌉ price calls per tick. Для 60k-SKU catalog с 1 store: 1 stock call + 1200 price calls @ 1 req/sec = ~20 минут на tick.
  • Catalog enumerator остаётся async-batch (SgGds job, ~часы) но бежит реже (catalog stable) и не блокирует commerce refresh.
  • EmitStockBeforePrices config knob удалён — early-emit branch оптимизировал прошлую медленную per-SKU stock-фазу; per-store dump заканчивается за секунды, branch deprecated.

Связанные ADR

  • ADR-0009 multi-tenant supplier credentials (warehouse_scope schema).
  • ADR-0043 ingestion orchestration (per-supplier scheduler tick).
  • ADR-0050 DKC adapter + federated taxonomy (другой connector pattern).
  • ADR-0060 (planned) multi-credential dispatcher + per-credential rate buckets.