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

63 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Мигратор МойСклад → 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. Проверить продажу/возврат/смену.
5. Откат: вернуть POS на МойСклад (данные там не трогались). Детали — отдельно при реализации Part 5–6.
## Статус
- [x] Part 1 (инвентаризация + цели сверки + gap-анализ).
- [ ] Part 2–7: спроектированы; реализация + прогон (часы) — продолжение после перехода-инфраструктуры.