# Commerce, МойСклад и маркетплейсы Сводные отчёты продаж, сотрудников и производства описаны в [руководстве по аналитике компании](COMPANY_ANALYTICS.md). Commerce-контур хранит нормализованные товары, внешние привязки, продажи, возвраты, расходы, остатки и локальные изображения товаров. Источником истины для структуры данных служат модели `backend/commerce/models/`, для HTTP API — `backend/api/v1/commerce/`, для импорта — provider-specific services в `backend/commerce/services/`. ## Импорт Пользовательский раздел «Товары» временно скрыт для дальнейшей разработки: пункт меню и выдача товаров в глобальном поиске отключены, адреса `/products` и вложенные страницы перенаправляют в ленту. Код экранов, данные и HTTP API сохранены; финансовая аналитика продолжает использовать импортированные факты. Управление доступно в Django admin → «Подключения интеграций» (`/admin/commerce/integrationconnection/`). Здесь можно добавить поддерживаемый сервис, заменить реквизиты, настроить автосинхронизацию и интервал. Пустые поля реквизитов сохраняют прежние значения; сохранённые секреты не показываются. Для проверки и ручного импорта отметьте подключение в списке и выберите «Проверить выбранные подключения» или «Синхронизировать выбранные подключения». Эти действия требуют права изменения подключений. Повторный импорт во время активного запуска не создаёт новую задачу. Прогресс и ошибки доступны в «Запусках синхронизации» (`/admin/commerce/integrationsyncrun/`). Проекты и выплаты по проектам также доступны в админке для просмотра; исправления импортированных документов вносятся в сервисе-источнике. Новые провайдеры требуют реализации коннектора; добавление одной записи источника не создаёт интеграцию автоматически. Синхронизация любого подключения запускается Celery-задачей `commerce.sync_connection_run`; сервис выбирается по `connection.provider`. Задача публикуется только после commit транзакции, а заранее сформированный `task_id` до публикации сохраняется и в подключении, и в истории запуска. Долгие импорты направляются в очередь `commerce`: worker должен слушать её отдельно либо вместе с очередью `celery`. Для работы ручного импорта нужен Celery worker, для расписания и восстановления зависших запусков — также Celery Beat. Импорт МойСклад выполняет следующие этапы: 1. `catalog` — загружает и идемпотентно нормализует ассортимент. 2. `images` — синхронизирует локальные изображения. 3. `variant_links` — восстанавливает связи модификаций с родителями. 4. `stocks` — пакетно обновляет остатки по складам. 5. `sales` и `returns` — импортирует отгрузки и возвраты с позициями. 6. `finalizing` — сохраняет итоговую статистику и состояние подключения. На каждом длительном этапе сохраняются `stage`, `stage_processed`, `stage_total` и `last_progress_at`; обновление отправляется frontend через общий WebSocket. Ошибка отдельного изображения учитывается в `image_errors`, но не отменяет импорт товарных и финансовых данных. Celery Beat раз в минуту выполняет reconciliation. Запуски, слишком долго оставшиеся в `queued`, и `running` без свежего heartbeat переводятся в `error` без удаления истории и без автоматического повторного выполнения. Таймауты задаются `COMMERCE_QUEUED_STALE_SECONDS` и `COMMERCE_RUNNING_STALE_SECONDS`. Ручная безопасная проверка: Долгие импорты имеют отдельные лимиты Celery: `COMMERCE_TASK_SOFT_TIME_LIMIT` (по умолчанию 90 минут) и `COMMERCE_TASK_TIME_LIMIT` (по умолчанию 120 минут). Мягкий лимит всегда должен быть меньше жёсткого. `SoftTimeLimitExceeded` не считается локальной ошибкой изображения: запуск завершается с явным сообщением о превышении времени. ```bash python manage.py reconcile_commerce_sync_runs --dry-run python manage.py reconcile_commerce_sync_runs ``` ## Wildberries Подключение принимает API-токен с категорией «Финансы». Проверка токена читает `GET https://common-api.wildberries.ru/api/v1/seller-info`, сохраняет ID, название магазина и ИНН без сохранения токена в открытом виде, а затем вызывает `GET https://finance-api.wildberries.ru/ping`, чтобы подтвердить доступ именно к финансовой категории. Финансовая синхронизация использует актуальный `POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed`. Отключённый 15 июля 2026 года legacy-метод `reportDetailByPeriod` не используется. Каждая строка отчёта сохраняется как `SourceRecord`, затем идемпотентно создаёт или обновляет `Sale`, `SaleLine` и связанные `Cost`. Поддерживаются продажи, возвраты, комиссия, эквайринг, доставка, хранение, приёмка, удержания, штрафы и расходы программы лояльности. Товар сопоставляется сначала по существующей WB-привязке, затем по артикулу и штрихкоду. Неоднозначные строки сохраняются, но не привязываются к случайному товару; их количество видно в `unmatched_products`. Повторный импорт после ручной привязки дополняет аналитику. Один ответ ограничен 100 000 строками. После каждой непустой страницы `rrdId` и границы периода сохраняются в metadata источника, а следующий запуск продолжает импорт. Завершённым отчёт считается только после ответа WB `204`; длина страницы не используется как признак конца. После завершения первичной истории каждый запуск повторно проверяет последние 30 дней, чтобы корректировки финансового отчёта обновили существующие факты. ## Изображения `ProductImage` хранит оригинал и миниатюру в `MEDIA_ROOT/commerce/products/`. Временные внешние ссылки МойСклад не возвращаются клиенту и не используются как публичные URL. Повторная синхронизация сравнивает внешний размер и время изменения, поэтому неизменённое изображение не скачивается повторно. Удалённые в источнике изображения удаляются локально после успешного чтения списка. Изображения сверх безопасного лимита декодирования уменьшаются изолированным процессом ImageMagick до допустимого размера, не увеличивая лимит Pillow в процессе worker. Ошибка одного файла оставляет остальные изображения товара и весь импорт работоспособными. REST `ProductSerializer` возвращает `images[]` с локальными `url` и `thumbnail_url`. Каталог использует миниатюру, карточка товара — оригинал для полноэкранного просмотра. ## Realtime и fallback Событие `commerce_connection_updated` содержит состояние подключения и последнего запуска, включая этап, heartbeat, прогресс и счётчики. При рабочем WebSocket frontend не опрашивает connections endpoint. Если WebSocket недоступен во время активного импорта, включается REST fallback раз в 30 секунд. ## Проверка ```bash cd backend pytest tests/commerce -q python manage.py makemigrations --check --dry-run python manage.py check ruff check commerce api/v1/commerce tests/commerce ```