# Мигратор МойСклад → 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: спроектированы; реализация + прогон (часы) — продолжение после перехода-инфраструктуры.