food-market/docs/migration-design.md
nns a2ee85bc12
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
docs(migration): инвентаризация источника МойСклад + дизайн мигратора (Part 1)
Решение владельца 09.10: полный перенос магазина из МойСклада в ALL MARKET с историей.
- scripts/migration/ms_inventory.py — read-only инвентаризация источника.
- docs/migration-source-inventory.md: орг «FOOD MARKET», ~31k товаров, 627 контрагентов,
  ~110k документов (97 707 чеков, 7 125 приёмок, 3 068 списаний…), 7 558 строк остатков,
  1 склад/точка/орг, касса наличных, 1 тип цены; gap-анализ модели + цели сверки.
- docs/migration-design.md: идемпотентность по msId, хронология документов, МСК→UTC,
  тиыны, резюмируемость, порядок этапов, сверка, дельта, runbook.
- docs/migration-progress.md: план Part 1–7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:26:35 +05:00

6.1 KiB
Raw Permalink Blame History

Мигратор МойСклад → ALL MARKET — дизайн

Основано на инвентаризации (migration-source-inventory.md): ~31k товаров, 627 контрагентов, ~110k документов (97 707 чеков). Существующий импортёр (MoySkladImportService) уже делает справочники product/productfolder/counterparty с идемпотентностью по MoySkladExternalId.

Принципы

  • Идемпотентность по msId: у КАЖДОЙ перенесённой сущности хранить MoySkladExternalId (уже есть у Product/ProductGroup — расширить на Counterparty/Store/RetailPoint/Employee/ UnitOfMeasure и ВСЕ документы + денежные операции). Повторный запуск — upsert по msId, без дублей (ключ — только id МойСклада; номера документов повторяются по годам).
  • Хронологический порядок документов (по moment), чтобы остатки/себестоимость/деньги складывались корректно: справочники → смены → приёмки/оприходования/возвраты поставщику → продажи/розн.возвраты → списания → инвентаризации → внесения/выплаты/ордера/платежи.
  • Время: moment МойСклада — МСК (UTC+3). Хранить в UTC = moment − 3ч. Фильтр дельты updated>= — тоже МСК.
  • Деньги: суммы МойСклада в ТИЫНАХ → ÷100. cashSum/noCashSum чека → нал/безнал оплата.
  • Авторы/статусы/связи: owner(кассир)→Employee; applicable→проведён; retailsalesreturn.demand→ исходный чек (связь «на основании»); комментарии (description)→Notes.
  • Резюмируемость: курсор по сущности (последний перенесённый msId/offset/updated) в migration_state (или ImportJob.progress). Падение → продолжение с места.
  • Прогресс/ошибки: на каждую сущность — счётчики ok/skip/err + журнал ошибок (ImportJob).

Порядок этапов (Hangfire, по шагам, каждый резюмируемый)

  1. uom → UnitOfMeasure (59; маппинг кода на наши ОКЕИ, иначе по имени, иначе создать).
  2. pricetype → PriceType (1 «Цена продажи» = системный).
  3. productfolder → ProductGroup (есть); product (+barcodes) → Product (есть); 31k постранично.
  4. counterparty → Counterparty (есть; флаги поставщик/покупатель).
  5. store → Store; retailstore → RetailPoint (+ денежный счёт кассы, наличные).
  6. employee → Employee/User (3; для owner чеков).
  7. Документы в хронологии (батчами по msId-страницам, expand≤100): supply, purchasereturn, enter, loss, inventory, retailshift, retaildemand, retailsalesreturn, retaildrawercashin/out, cashin, paymentout. Каждая позиция: assortment→Product по msId, qty, price(тиыны), discount.

Производительность

  • Чтение: лимит 45 req/3с, таймаут ≥120с, повтор страниц ≤4. 110k докумов страницами по 100 (+expand) ≈ тысячи запросов → десятки минут чтения; запись батчами в БД. Полный прогон — несколько часов, поэтому фоновый Hangfire с резюмируемым курсором, не блокирующий.
  • Повторный запуск: upsert по msId, 0 дублей; измерять время в ImportJob.

Сверка (Part 4, авто → migration-reconciliation.md)

Цели (из источника, read-only):

  • Остатки: по товар×склад report/stock/all (7558 строк; нулевые НЕ отдаёт) == наши остатки.
  • Долги: report/counterparty (взаиморасчёты) == наши долги поставщиков/покупателей.
  • Деньги: остаток кассы точки (чеки+внесения−выплаты−ордера) == МойСклад.
  • Выручка/чеки по месяцам: Σ retaildemand.sum и count по месяцам == наши.
  • Число документов каждого типа == таблица инвентаризации. Расхождения — разобрать построчно/объяснить.

Дельта (Part 5)

После полного переноса — Hangfire раз в N минут: filter=updated>=<последний_курсор МСК> по каждому типу + удалённые (через /audit или сверку id-множеств) → upsert/удаление. В день перехода — финальный прогон дельты, сверка, переключение кассы на ALL MARKET.

Runbook дня перехода (Part 7) — кратко

  1. Накануне: полный перенос + сверка = 0. 2. Утром: закрыть смену в МойСкладе, финальная дельта, сверка кассы/остатков. 3. Переключить POS на ALL MARKET-орг. 4. Проверить продажу/возврат/смену.
  2. Откат: вернуть POS на МойСклад (данные там не трогались). Детали — отдельно при реализации Part 5–6.

Статус

  • Part 1 (инвентаризация + цели сверки + gap-анализ).
  • Part 2–7: спроектированы; реализация + прогон (часы) — продолжение после перехода-инфраструктуры.