food-market/docs/migration-runbook.md
nns 110ecf551d
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
feat(migration): дельта-драйвер + рунбук дня перехода + сверка балансов/долгов
- scripts/migration/delta_sync.py: раз в N мин updated>=lastSync по всем типам,
  удаления через /audit, состояние в ~/.fm-watchdog, перекрытие окна 2 мин
- docs/migration-runbook.md: фазы A/B/C, откат на МойСклад, надёжность, ограничения токена
- reconcile.py: секции балансов счетов (report/money/byaccount) и долгов (403-ограничение)
- progress: Parts 5/6/7 закрыты

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 16:36:14 +05:00

9 KiB
Raw Blame History

Рунбук «День перехода» — МойСклад → ALL MARKET

Решение владельца 09.10.2026: перенести ВСЁ из МойСклада с историей и продолжить работу в ALL MARKET. Этот документ — пошаговый регламент перехода, кто что делает, как откатиться и что проверить после. Фискализации/маркировки нет (не блокеры).

  • Источник: аккаунт МойСклад, орг «FOOD MARKET» (bb8f74d4-5eb6-11ef-0a80-106e00048af9). Только чтение.
  • Приёмник: ALL MARKET, орг «FOOD MARKET» на стенде (простой режим), admin foodmarket-migr@food-market.local. Демо-магазин и «Магазин у дома» НЕ трогаем.
  • Токен МойСклада — ~/.food-market-secrets/moysklad.env (в МойСклад ничего не писать).

Фаза A. Заранее (за несколько дней до перехода)

  1. Полный перенос истории (один раз, многочасовой). Запускать ПОСЛЕДОВАТЕЛЬНО (одна орг, гонка остатков при параллельном запуске):
    • Справочники: товары, группы, контрагенты (несут MoySkladExternalId).
    • Документы (хронология, МСК→UTC, суммы ÷100, авторы/статусы): supply → enter → loss → purchasereturn → retaildemand → retailsalesreturn → inventory.
    • Деньги + плуг балансов: type=money (платежи/ордера/внесения/выплаты + OpeningBalance).
    • Эндпоинт: POST /api/admin/moysklad/import-supplies?type=<T>[&offset=N].
  2. Сверка (scripts/migration/reconcile.py → docs/migration-reconciliation.md): число документов, остатки товар×склад, выручка+чеки по месяцам, балансы счетов, долги. Все расхождения = 0 или объяснены построчно.
  3. Включить дельту (scripts/migration/delta_sync.py, cron раз в 10–15 мин) с момента окончания полного переноса: подтягивает только изменения updated>=lastSync. Проверить, что свежие чеки из МойСклада появляются в ALL MARKET в течение интервала.

Фаза B. День перехода (час X)

Шаг Кто Действие
B1 Владелец Объявить стоп-кассе в МойСкладе: последний чек пробит, новые продажи приостановлены на ~15 мин.
B2 Инженер Остановить cron дельты (чтобы зафиксировать окно).
B3 Инженер Финальная дельта вручную: python3 scripts/migration/delta_sync.py — подтянет последние чеки/платежи за минуты.
B4 Инженер Обработать удаления из отчёта дельты (lastDeletions в состоянии): удалить соответствующие документы в ALL MARKET по регламенту (сверять по MoySkladExternalId).
B5 Инженер Прогнать сверку reconcile.py. Убедиться: документы, остатки, выручка, балансы = 0 расхождений (долги — см. ограничение токена ниже).
B6 Владелец+Инженер Контрольная ручная проверка в UI: 3–5 товаров (остаток), 2–3 поставщика (долг), баланс кассы и расчётного счёта, выручка за вчера.
B7 Владелец Переключить персонал/кассы на ALL MARKET (POS → адрес нового сервера, логины кассиров).
B8 Владелец Возобновить продажи в ALL MARKET. МойСклад переводится в режим «только архив» (не продавать).

Фаза C. После перехода (контроль)

  • Первый день: следить за остатками/выручкой в ALL MARKET, сверять итоги дня с ожиданиями.
  • Держать аккаунт МойСклада доступным на чтение ≥1 месяц (архив истории, разбор спорных).
  • Не удалять токен/данные МойСклада, пока переход не признан успешным.

Откат (rollback) — вернуться на МойСклад

Переход спроектирован как неразрушающий для источника: в МойСклад мы НИЧЕГО не писали, поэтому откат = продолжить работать в МойСкладе.

  1. Владелец объявляет возврат на МойСклад; кассиры переключают POS обратно на МойСклад/старую кассу.
  2. Продажи, совершённые в ALL MARKET за время после часа X, переносятся в МойСклад вручную (их немного — окно короткое) ИЛИ принимается, что они останутся только в ALL MARKET как параллельный период (по решению владельца).
  3. Орг ALL MARKET на стенде остаётся как есть (данные не удаляются) — для повторной попытки.
  4. Причина отката фиксируется; дефект устраняется; повторный день перехода назначается заново.

Критерий «не откатываемся»: сверка B5 = 0 расхождений (или объяснены) и ручная проверка B6 совпала. Если расходится критично (остатки/деньги) — откат по пунктам выше.


Надёжность и производительность (Part 6)

  • Идемпотентность: повторный запуск любого типа пропускает уже перенесённое по MoySkladExternalId (дублей нет). Проверено на приёмках/оприходованиях/списаниях.
  • Резюмируемость: падение/рестарт в середине — продолжить с ?offset=N (N ≈ число уже перенесённых минус 200 для перекрытия); идемпотентность убирает повтор.
  • Устойчивость к сбоям: каждый документ — отдельный SaveChanges + ChangeTracker.Clear (в т.ч. в catch), поэтому ошибка одного документа не роняет прогон и не каскадит.
  • Лимиты МойСклада: 45 запросов/3с, таймаут 130с, Accept-Encoding: gzip, 429 с повтором по X-RateLimit-Retry-TimeInterval. Страница 100 док., order=moment,asc.

Известные ограничения (объяснённые расхождения)

  • Долги контрагентов: отчёт МойСклада report/counterparty недоступен выданным read-only токеном (403 — нет права на взаиморасчёты). Долги в ALL MARKET формируются из перенесённых первичных документов; итоговую цифру сверять вручную в UI МойСклада (шаг B6) или временно выдать токену право на отчёт для авто-сверки reconcile.py (секция 5).
  • Балансы счетов: розничная наличность/движения кассовых смен до перехода не материализуются подокументно; баланс сводится «плугом» OpeningBalance = баланс МойСклада (report/money/byaccount) − Σ перенесённых проводок. Текущий баланс счёта в ALL MARKET точно совпадает с МойСкладом; подробная история доналичных движений остаётся в архиве МойСклада.
  • Нулевые остатки: report/stock/all не отдаёт нулевые позиции — товар без строки считается остатком 0 (сверка это учитывает).