Модуль МойСклад: устройство
Как этим пользоваться — в разделе для склада. Здесь — что внутри.
Состав
apps/backend/src/modules/moysklad-integration:
| Файл | Ответственность |
|---|---|
client.ts | HTTP-клиент JSON API МойСклад |
credentials.ts | шифрование реквизитов (MOYSKLAD_ENCRYPTION_KEY, см. ниже) |
service.ts | модульный сервис Medusa |
retry-schedule.ts | расписание повторов |
stock-sync-report.ts | отчёт о цикле синхронизации остатков |
models/moysklad-connection-config.ts | подключение: реквизиты, флаг активности |
models/moysklad-order-sync.ts | состояние отправки конкретного заказа |
Реквизиты — токен либо логин/пароль — хранятся в базе в зашифрованном виде, не в переменных
окружения. Шифруются AES-256-GCM: MOYSKLAD_ENCRYPTION_KEY из окружения сначала хешируется
SHA-256 (получая ровно 32 байта под ключ), результат хранится в версионированном контейнере
v1 со случайным 12-байтовым IV и auth tag от GCM (credentials.ts:21). Смена
MOYSKLAD_ENCRYPTION_KEY делает сохранённые реквизиты нечитаемыми — расшифровка требует того же
ключа, каким шифровали.
configureMoyskladConnection (workflows/steps/moysklad-connection.ts:24) при успешной
проверке реквизитов сразу выставляет is_active=true — сохранение работающих реквизитов и
включение обмена не два раздельных шага, а один.
Сам шаг только проверяет реквизиты и ставит is_active=true — никакой stock/order sync workflow
он не вызывает. Остатки подтянутся при ближайшем срабатывании 15-минутного moysklad-stock-sync
(см. ниже), заказы — по новым событиям или очередному минутному retry-job. После включения
интеграции обновления по факту приходят не сразу, а в пределах этих интервалов.
Ключ сопоставления
Товары сопоставляются по коду номенклатуры: SKU варианта Medusa ищется как
filter=code=<sku> в /entity/product. Не по названию и не по артикулу. Вариант без SKU в
синхронизации не участвует.
Остатки: jobs/moysklad-stock-sync.ts
Каждые 15 минут. Постранично (200 записей) собираются варианты и их уровни запаса, тянутся остатки из МойСклад и записываются в стандартный inventory Medusa. Заказы Medusa, участвующие в вычитании ниже, загружаются той же стабильной постраничной схемой — по 200 записей до полного обхода.
Дополнительно вычитаются количества из заказов сайта за последние 48 часов
(STOCK_RESERVATION_WINDOW_MS): МойСклад ещё не знает об этих заказах, пока сотрудник их не
провёл, и без вычитания остаток был бы завышен. Учитываются только неотменённые заказы,
распознанные как оформленные через эту витрину — по metadata.checkout_contact_name
(jobs/moysklad-stock-sync.ts:148). Результат принудительно ограничивается снизу нулём
(:389).
Он всегда вычисляется из стандартного inventory: остаток > 0 → «в наличии»; остаток 0 при разрешённом backorder → «под заказ»; иначе «нет в наличии». Отдельного поля со статусом не существует, и заводить его не нужно.
Изоляция и параллелизм. Процессовый флаг не даёт двум циклам синхронизации выполниться
одновременно — новый цикл пропускается, пока предыдущий ещё выполняется
(jobs/moysklad-stock-sync.ts:29,452). Внутри одного цикла ошибка одной позиции не останавливает
применение остальных остатков — все ошибки агрегируются и завершают цикл общим статусом сбоя
(:395).
Диагностика несопоставленных остатков. stock-sync-report.ts классифицирует причину каждого
пропуска: отсутствующий/неоднозначный код МойСклад, неоднозначный SKU Medusa, отсутствие
соответствия. Видна в админке на странице интеграции (см. для склада).
Заказы: подписчики + повторы
subscribers/moysklad-order-placed.ts отправляет заказ сразу при оформлении;
subscribers/moysklad-order-canceled.ts помечает отмену. Задача
jobs/moysklad-order-sync-retry.ts раз в минуту добирает то, что не прошло.
(*) Ошибка отправки не откатывает уже оформленный заказОшибка первичной отправки заказа в МойСклад при оформлении не откатывает заказ
(subscribers/moysklad-order-placed.ts:14) — заказ остаётся оформленным, синхронизация просто
уходит в статус ошибки и ждёт фонового повтора. Ошибка передачи отмены в МойСклад точно так же
не откатывается (subscribers/moysklad-order-canceled.ts:14). Это сознательное решение: сбой
интеграции не должен блокировать коммерческую операцию, transaction boundary заказа и
синхронизации разделены намеренно.
Идемпотентность ручного повтора. Повтор сначала исправляет ошибку передачи отмены и
только при её отсутствии повторяет создание заказа (steps/retry-moysklad-order-sync.ts:19).
Если внешний заказ в МойСклад уже создан, но локальный финальный статус не записался (например,
процесс упал между POST и записью результата), повтор находит существующий внешний ID и
помечает синхронизированным без второго POST (steps/create-moysklad-customer-order.ts:78) —
защита от дублей заказов в МойСклад при повторных попытках.
Повторное включение интеграции сбрасывает исчерпанные попытки создания и отмены заказа до
первой ступени расписания повторов (service.ts:84) — выключил/включил не оставляет заказы
«застрявшими» в исчерпанном лимите попыток.
Расписание повторов фиксированное — 1 мин, 5 мин, 30 мин, 2 ч, 12 ч, то есть 6 попыток
всего вместе с первой, синхронной. Дальше заказ остаётся в статусе error и ждёт ручного
повтора из админки.
Запись syncing, брошенная упавшим процессом, считается заброшенной через 10 минут
(STALE_SYNCING_MINUTES) и снова становится доступной для повтора — иначе она зависла бы навсегда.
У moysklad_order_sync два независимых набора полей: status/attempts для создания и
cancellation_status/cancellation_attempts/cancellation_updated_at для отмены. Так повтор
отмены не сбрасывает status уже отправленного заказа и не выглядит как «заказ не
синхронизирован». Своя временная метка у отмены нужна, чтобы сверка на стороне создания не
сбрасывала проверку срока повтора.
Что не синхронизируется регулярным обменом
Цены, названия, изображения и категории живут в Medusa и в МойСклад не ездят. Из МойСклад регулярным обменом берутся только остатки; в МойСклад уходят только заказы.
Это ограничение касается именно постоянной синхронизации (эта страница). Разовый перенос
товаров, категорий, цен и изображений из МойСклад в Medusa существует, но отдельным
CLI-инструментом, не автоматическим обменом — см. import-moysklad-catalog.ts в Служебных
скриптах.
Клиент: лимиты и таймауты
client.ts:
- Глобальный rate limit — все экземпляры совместно допускают вес запросов не более 45 за 3
секунды; отчёт остатков имеет вес 5 (
client.ts:9). - До трёх повторов на HTTP 429, задержка из заголовка
x-lognex-retry-after, при его отсутствии — 3 секунды (client.ts:527). MOYSKLAD_BASE_URLменяет адрес API;MOYSKLAD_REQUEST_TIMEOUT_MS— стандартный 30-секундный таймаут запросов (client.ts:168).
Грабли реального API
Реальный API строже моков — по заголовкам, ключу сопоставления и лимитам. Подробности
и другие особенности интеграций — docs/known-quirks.md.