- 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>
91 lines
9 KiB
Markdown
91 lines
9 KiB
Markdown
# Рунбук «День перехода» — МойСклад → 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 (сверка это учитывает).
|