Перейти к основному содержимому

Модуль МойСклад: устройство

Как этим пользоваться — в разделе для склада. Здесь — что внутри.

Состав​

apps/backend/src/modules/moysklad-integration:

ФайлОтветственность
client.tsHTTP-клиент 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.