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):
| Path | Shape | Verdict |
|---|---|---|
/goods/{ids%2C…id50}/price?type=etm | batch 50 OK, returns 50 rows | OK |
/goods/{ids%2C…}/remains | 404 «Товар не найден» | NOT SUPPORTED |
/goods/{id}/remains?type=etm | single SKU; full payload InfoStores+InfoForecast+InfoSuppStores+DeliveryTime | OK (single only) |
/goods/remains?store=N1,N2 | до 2 store codes / call; flat rows {StoreCode, GdsCode, Article, RemInfo}, только positive remains | OK |
/goods/remains?store=N1,N2,N3 | 400 | NOT 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. Сейчас sharedpolicy.Databucket общий для всех 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.
EmitStockBeforePricesconfig 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.