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

91 lines
9 KiB
Markdown
Raw 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
Решение владельца 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 (сверка это учитывает).