Перейти к основному содержимому

0059. Синк каталога по требованию и онбординг-хук подключения кабинета

Копия ADR из репозитория: backend/docs/adr/0059-catalog-sync-on-demand-and-onboarding-hook.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-09-28
  • Связанные задачи: [BE] Catalog: HTTP sync + первичное наполнение SKU при подключении кабинета

Контекст и проблема​

Общий каталог товаров (ADR-0052) наполнялся только расписанием: карточки — cron 04:00 MSK, цены — каждые 30 минут. У только что подключённого кабинета GET /pricing/products и GET /repricer/products оставались пустыми до утра, а у пользователя не было кнопки «Синхронизировать».

Два ограничения:

  • синк цен пишет только в существующие карточки — на пустом каталоге он бесполезен, пока не прошёл синк карточек;
  • CatalogModule импортирует WbAccountsModule, поэтому wb-accounts не может импортировать каталог (циклическая зависимость Nest-модулей). @nestjs/event-emitter в проекте нет.

Рассмотренные варианты​

  1. Две джобы (карточки + цены) в одну очередь — просто, но порядок не гарантирован: цены на пустом каталоге ничего не запишут.
  2. Отдельная джоба «полный синк» — карточки, затем цены в одном прогоне.
  3. Онбординг: forwardRef между модулями — работает, но закрепляет цикл и делает wb-accounts зависимым от всех будущих потребителей.
  4. Онбординг: регистрировать очередь каталога в WbAccountsModule — без цикла модулей, но wb-accounts знает про имя очереди и формат джобы каталога.
  5. Онбординг: подписка через WbAccountsService.onKeyAdded — минимальный in-process хук; потребители подписываются в onModuleInit.
  6. Онбординг: подключить @nestjs/event-emitter — новая зависимость ради одного события.

Решение​

  • Джоба catalog:sync-account-full (вариант 2) в очереди catalog-sync, исполняется CatalogSyncProcessor теми же импортёрами, что и расписание. jobId детерминирован по кабинету — анти-дубль как у POST /inventory/sync: живая джоба возвращает alreadyRunning: true, упавшая/завершённая переставляется с полным ретрай-бюджетом.
  • HTTP: POST /catalog/sync (202 + { jobId, alreadyRunning }), GET /catalog/sync/status, алиас GET /catalog/products.
  • Онбординг (вариант 5): WbAccountsService после создания кабинета и после смены ключа вызывает подписчиков fire-and-forget — промис не ожидается, ошибки только логируются. Недоступный Redis или зависший queue.add не роняет и не задерживает POST /wb-accounts. CatalogSyncJobsService подписывается в onModuleInit и ставит полный синк в системном контексте.

Последствия​

  • Положительные: каталог нового кабинета появляется через минуты, а не к утру; ручной и плановый синк идут одним кодом и одним rate-limit бюджетом WB; хук переиспользуем (например, первичный синк остатков) без новых зависимостей у wb-accounts.
  • Отрицательные / компромиссы: хук in-process и без гарантии доставки — если процесс упадёт между сохранением кабинета и постановкой джобы, каталог наполнит ближайший cron (или кнопка «Синхронизировать»). state в GET /catalog/sync/status отражает только джобу по требованию, не плановые прогоны; свежесть данных — lastSyncAt (последний синк цен каталога).
  • Дальше: при появлении второго-третьего подписчика — рассмотреть @nestjs/event-emitter вместо ручного хука.