{{COUNT_ROUTES}} API endpoints {{COUNT_PAGES}} страниц UI {{COUNT_ADRS}} ADR

Обзор проекта

BCP — UI-прототип и backend для WMS / маркетплейс-fulfillment платформы (Next.js 15 + Postgres).

BCP — тестовый сервер

UI-прототип BCP (Next.js 14). Развёрнут на этой машине для просмотра/демо.

Что и где

Хостwms-app (89.23.35.71 / 10.0.0.140)
Папка проекта~/bcp/ (/home/wms/bcp)
Процесс-менеджерpm2, имя процесса bcp
Конфиг pm2~/bcp/ecosystem.config.js
Режим запускаcluster, 8 инстансов (по числу ядер), max_memory_restart: 1G
Порт7000, pm2 балансит round-robin между инстансами
Доступ изнутри LANhttp://10.0.0.140:7000
Доступ снаружиhttp://89.23.35.71:7000 — только если на роутере добавлен NAT-форвард :7000 → 10.0.0.140:7000
Источник APIapiinpacking.ru через серверные прокси-роуты /api/bcp/* (admin_panel) и /api/todo/* (todo)
Авто-старт после ребутада, через systemd-юнит pm2-wms.service

Команды на каждый день

pm2 list                  # 8 инстансов в кластере
pm2 logs bcp              # хвост логов в реальном времени (Ctrl+C)
pm2 logs bcp --lines 100  # последние 100 строк, без follow
pm2 reload bcp            # zero-downtime перезапуск (по одному инстансу)
pm2 restart bcp           # жёсткий рестарт всех инстансов сразу
pm2 stop bcp              # остановить
pm2 start ecosystem.config.js  # запустить остановленный
pm2 monit                 # интерактивный монитор (CPU/RAM/логи)

Обновление кода — всегда pm2 reload bcp (а не restart), пока не упёрся в проблему. Reload не рвёт активные http-коннекты.

Лог-файлы pm2: ~/.pm2/logs/bcp-out.log и ~/.pm2/logs/bcp-error.log.

Окружение

~/bcp/.env.local (основные переменные):

# Frontend
NEXT_PUBLIC_API_URL=https://apiinpacking.ru/admin_panel/hs/v1
NEXT_PUBLIC_TODO_API_URL=https://apiinpacking.ru/todo/hs/v1
NEXT_PUBLIC_USE_MOCK=false
NEXT_PUBLIC_APP_NAME=BCP

# AI integration (3-provider chain — see knowledge/05-Decisions/2026-05-14-bcp-ai-integration-multi-provider.md)
# Priority 1: ChatGPT Pro proxy ($0 через подписку, primary)
CHATGPT_PROXY_BASE_URL=https://api.vibe-dev.team/chatgpt-proxy/v1
CHATGPT_PROXY_API_KEY=cli-proxy-local-bot-key

# Priority 2: OpenAI direct ($0.003-0.005/call, резерв)
OPENAI_API_KEY=sk-proj-...
OPENAI_MODEL=gpt-5.5

# Priority 3: OpenRouter (base + 5-10%, резерв-резерва + доступ к Claude/Gemini/Llama)
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_MODEL=openai/gpt-5.5

# Email SMTP (для notify через wms_notification_log channel='email' +
# критичных events — invoice.issued/paid/overdue, request.fbs.completed)
SMTP_HOST=smtp.yandex.ru
SMTP_PORT=465
SMTP_SECURE=1
SMTP_USER=noreply@5st.pro
SMTP_PASS=...
SMTP_FROM=5 stars <noreply@5st.pro>

# Sentry (error tracking + Telegram bridge)
SENTRY_DSN=https://...@o*.ingest.de.sentry.io/...
SENTRY_ORG=saddam-7ou
SENTRY_PROJECT=bcp-wms
SENTRY_ENVIRONMENT=production
SENTRY_WEBHOOK_SECRET=bcp_sentry_webhook_secret_2026  # для Sentry → Telegram bridge
SENTRY_AUTH_TOKEN=sntrys_...  # для source maps upload при build

# Telegram bot (для уведомлений + admin alerts)
WMS_TG_BOT_TOKEN=8303615547:...
WMS_TG_ADMIN_CHAT_ID=...  # для AI health-check + critical alerts

# Cron (для /api/cron/* endpoints — storage-billing, ai-health-check, etc.)
WMS_CRON_SECRET=...

После правки env:

pm2 restart bcp --update-env   # restart с пересохранением env (reload не подхватывает .env.local)

Обновление кода

Сейчас код синхронизируется агентом (Claude) с локальной машины разработчика через tar | ssh "tar -x" в ~/bcp/, затем на сервере:

cd ~/bcp
npm ci                       # только если изменился package-lock.json
npx next build --no-lint     # пересборка production-bundle (lint выключен — это тестовая среда)
pm2 reload bcp               # zero-downtime

В планах — перевести синхронизацию на git-pull, тогда обновление будет одной командой ~/bcp/deploy.sh.

Распределение ресурсов

СервисRAMCPU
BCP cluster ×8~720 MB idle, до ~1.5 GB при пикевсе 8 ядер при пике
Свободно12+ GB / большая часть CPU

Если на этой же машине поднимется 1С + PostgreSQL — UI можно сжать до 4 инстансов: в ecosystem.config.js поменять instances: "max" на 4, потом pm2 reload bcp.

Заметки

  • npm run build (без --no-lint) сейчас падает из-за ESLint-правила no-unused-vars в src/components/tasks/compact/*. На тестовом сервере мы это игнорируем флагом --no-lint. Локально у разработчика правила линта остаются включёнными.
  • Сборку и npm ci нельзя запускать параллельно с уже работающим pm2 start bcp без последующего reload — pm2 нужно либо reload, либо restart после build. Иначе старый процесс держит файлы из .next/, новые перезапишут — белый экран до следующего рестарта.
  • Если pm2-демон случайно умер: pm2 resurrect поднимет всё из ~/.pm2/dump.pm2 (последний pm2 save).
  • Если правишь ecosystem.config.js — после правки pm2 delete bcp && pm2 start ecosystem.config.js && pm2 save.

Полная переустановка с нуля

Если что-то развалилось:

pm2 delete bcp
rm -rf ~/bcp/node_modules ~/bcp/.next
cd ~/bcp
npm ci
npx next build --no-lint
pm2 start ecosystem.config.js
pm2 save

Последние изменения

Unreleased + три последние версии из CHANGELOG.md.

[Unreleased]

[Unreleased]

Added — 2026-05-18 (Wave 40 part 3 — auto-generated docs pipeline + /docs route)

BCP now has a single-file HTML documentation pipeline like the bot's, generated from local sources (CHANGELOG, README, route handlers, dashboard pages, ADR vault). Onboarding new tenants and engineers gets an offline-readable reference without spelunking the repo.

Pipeline:

  • ops/generate-docs.ts (~530 lines) — pure-TypeScript generator using only Node built-ins (no new npm deps). Walks src/app/api/**/route.ts and extracts JSDoc above each export async function GET/POST/... for method + description. Walks src/app/(dashboard)/**/page.tsx and pulls <PageHeader title="" description="">. Reads CHANGELOG.md ([Unreleased] + last 3 versioned/date sections, body truncated at 8 KB), README.md (rendered with a minimal GFM-compatible markdown renderer including tables), and knowledge/05-Decisions/*.md (titles from H1, links left as local file paths).
  • Output: docs/index.html — self-contained ~438 KB, no external assets.
  • Build time: ~50 ms over 408 route files + 184 page files.

HTML template (inlined in the generator):

  • Dark mode default (matches BCP brand: --accent: #c94545, system theme respected via prefers-color-scheme). Theme toggle persists to localStorage.
  • Sidebar with search input + 5 sections (Overview, Changes, API Reference, Pages, ADRs) and per-section counts.
  • API routes grouped by top-level segment (auth, cron, platform, telegram, tsd, webhooks, wms, ...), each route is a collapsible <details> card with colored method badge (GET green / POST blue / PUT amber / PATCH purple / DELETE red), path, 1-line description, and the full JSDoc body + source file path.
  • Pages section: tables per first-segment, columns = route / title / description / source file.
  • ADRs section: chronological list with date badge + local file link (open from C:\vibe-dev\knowledge\05-Decisions).
  • Client-side search (sidebar input) — highlights matches and hides non-matching cards/rows in the active section. Debounced 80 ms.
  • Print-friendly: all sections expand, route-cards force-open, navigation hidden.

npm scripts (package.json):

  • docs:buildtsx ops/generate-docs.ts
  • docs:servenpx serve docs (optional local preview)

Serving on prod — added src/app/docs/route.ts (Next.js route handler, nodejs runtime, force-static):

  • GET /docs → reads docs/index.html from disk on cold start, caches in module scope keyed by mtime (re-reads only when docs:build updates the file). Returns 200 with Content-Type: text/html; charset=utf-8 + Cache-Control: public, max-age=300.
  • If docs/index.html is missing → 404 with build instructions HTML (so deployments without the artifact give a clear error).
  • Public (no auth) — same model as the bot's bot.vibe-dev.team/docs.

Coverage statistics (current snapshot):

  • 408 API endpoints documented (265 with JSDoc, 143 still missing — opportunity to add /** ... */ over handlers).
  • 184 dashboard pages (100 % title coverage via PageHeader extraction or filename fallback; 52 % with descriptions).
  • 57 ADRs linked.
  • 3 changelog sections rendered (Unreleased + 2 dated).

Smoke test: npm run devcurl http://localhost:7000/docs returns 200 with 448 KB HTML in ~1 s (cold), ~5 ms (warm cache).

Changed — 2026-05-18 (Wave 40 part 1 — API error strings i18n with per-request locale)

API error messages in the 10 most-trafficked route handlers are now localized. Previously all message fields were hardcoded Russian strings. They now resolve to the user's preferred language via Accept-Language header → user preferredlocale → tenant defaultlocale → 'ru' fallback. The machine-readable error field is unchanged (backward-compatible).

New files:

  • src/lib/wms/api-errors.tsapiError() helper: resolves locale, loads translation, returns localized NextResponse. Also exports interpolateMessage() and getLocalizedMessage().
  • tests-unit/lib/wms/api-errors.test.ts — 23 unit tests covering all locale resolution paths + response shape.

Modified — i18n:

  • src/lib/wms/i18n-server.ts — added getRequestLocale(req): Accept-Language header → user preferredlocale → tenant defaultlocale → 'ru'.
  • public/locales/{ru,en,uz,kz}/common.json — added errors.* namespace (22 keys each): not_a_seller, not_a_seller_respond, forbidden_not_owner, no_pending_proposal, invalid_status_propose, proposal_pending, invalid_status_complete, not_wb_marketplace, no_wb_token, wb_supply_not_found, forbidden_seller_classify, forbidden_seller_unclassify, supply_not_completed, forbidden_staff_only, box_not_found, fbo_request_not_found, cargo_already_bound, box_belongs_to_other_request, platform_user_not_found, forbidden_manager_plus, duplicate_return.

Modified — routes (migrated from hardcoded RU message to apiError()):

  • src/app/api/wms/fbo/supplies/seller-create/route.ts
  • src/app/api/wms/fbo/supplies/[id]/propose-date/route.ts
  • src/app/api/wms/fbo/supplies/[id]/respond-proposal/route.ts
  • src/app/api/wms/fbo/supplies/[id]/complete/route.ts
  • src/app/api/wms/fbo/supplies/[id]/bind-wb/route.ts
  • src/app/api/wms/returns/[id]/classify/route.ts
  • src/app/api/wms/returns/[id]/unclassify/route.ts
  • src/app/api/wms/fbo/returns/from-supply/[supplyId]/initiate/route.ts
  • src/app/api/wms/tsd/fbo/scan-bind/route.ts
  • src/app/api/wms/account/preferred-locale/route.ts

Modified — tests (updated rbac mocks: requireRoleactorHasRole for routes that migrated):

  • tests-unit/api/wms/storage-billing.test.ts
  • tests-unit/api/wms/fbo/propose-date.test.ts
  • tests-unit/api/wms/tsd/scan-bind.test.ts

Acceptance: POST /api/wms/fbo/supplies/seller-create with a non-seller session + Accept-Language: en header → response body message is in English. Same endpoint without Accept-Language → message in ru (or user's preferred_locale from DB). error field (not_a_seller) is unchanged.

Added — 2026-05-18 (Wave 40 part 5 — Sentry alert rules + observability hardening)

Hardened the Sentry pipeline for the endpoints shipped in Waves 29-39. The Wave 20 wiring already gives us auto-instrumentation + a Telegram bridge, but until this part there were no project-specific alert rules for the new FBO / MAX / cron paths and no documentation around what tags we emit.

New ops files (no src/ route handler changes):

  • ops/sentry/alert-rules.yaml — 13 rules across 6 categories. Critical: FBO seller-create high error rate (>5% / 5min), FBO supply complete 5xx (immediate), MAX webhook 5xx (immediate), fbo-proposal-reminder cron silence >7h, cleanup-expired-tokens cron silence >25h, MAX webhook signature failures >3/min. Performance: p95 latency >2s on /api/wms/fbo/*, p99 >5s on /api/wms/fbo/metrics. Multi-tenant: cross-tenant 403 spike. DB: ECONNREFUSED / too many connections (pager). Plus a catch-all baseline.
  • ops/sentry/sentry-dashboard.json — 9 widgets: critical errors by route, top slow endpoints, MAX webhook success rate (24h), cron success rate (7d), cross-tenant 403s, FBO supplies funnel, errors by tenant_role, top FFs by error volume, MAX webhook signature failures.
  • ops/sentry/README.md — manual apply procedure (Sentry has no clean YAML import API), tag glossary, notification channel setup (Telegram primary via existing forwardToLocalBridge, email fallback, optional Slack escalation), escalation policy (L1=Telegram 0min, L2=email 15min, L3=Slack 60min), smoke-test commands.

Instrumentation tweaks (project-root config files only, NOT src/):

(...секция обрезана — полный текст в CHANGELOG.md)

2026-05-03 (closed)

2026-05-03 (closed)

Added

  • feat(fbs): counters в табах = количество заказов (orders), а не supplies (25a1bd7). Для FBS-поставщика логичнее видеть «5 заказов» а не «1 поставка с 5 заказами». Бейджи на табах «На сборке / В доставке / Готово / Архив» теперь считают orders.
  • fix(fbs): seller-warehouse фильтр + auto-backfill в sync (7bcc1c1). Если у клиента подключено несколько складов отгрузки, sync теперь корректно фильтрует supplies по выбранному. Auto-backfill при первом синке.
  • fix(fbs): supply tasks теперь синкаются (77f00b3). 5 orders внутри supply WB-GI-... теперь видны как wms_request_item с external_task_id. Раньше — пустой supply.

2026-05-02

2026-05-02

Added

  • Big-batch features (39850fc): dashboard charts, trial-cancel, quotas, bulk-invite, KPI timeseries.
  • Mega-batch (50f3079): auth/audit/webhooks/imports/forecasts/stickers.
  • OZON scopes + token rotation + CSV-tariffs + DBS UI + stage labels + QR labels + PgBouncer (e97d915).
  • Big-sprint-2 (7bdc301): storage billing daily, invoice lines flow, bulk transitions, WB sync, ErrorBoundary.
  • Big sprint (2e5ba66): WB-canon stage panel, KPI banners, invoice register, toasts.
  • End-of-month close, email notif, sentry-lite, DBS courier fields (628bd15).
  • WB-canon recon + ClientGuard + cabinet auto-pickup (b22fc7e).
  • FBS WB-aligned UX (94a6af1, ee4282f): clearer pipeline, hint card, dropdown actions.
  • Client-mode seller cabinet (ea04537): restricted sidebar для клиентов ФФ.
  • Unified SVG label engine + LabelPrintDialog (3ba1945).
  • OZON sync-all batch + Stripe webhook placeholder + toast вместо alert (da47d8d).
  • BCP task transformer P1-P4 / completed / duedate / assigneeid (30ea4bb).
  • GenericResource UI + OZON sync UI + System extras (e3b8f72).
  • Sprint 11/11: signup + token validation + reconciliation + admin/health (6e222f1).
  • WB-validator (7de602a): scope-aware token validation + cron-fix + re-encrypt.

Fixed

  • fbs-sync: trust WB — sync only active /orders/new (5b88a1c), drop pullOrdersHistory ingestion.
  • fbs-sync: reconciliation — stale draft orders auto-canceled (4f544a3).
  • fbs: remove shipment-type sub-tab + show product/sticker columns (WB-style) (23a9dfe).
  • storage-billing: COALESCE numeric/text mismatch в storage_svc CTE (c225d32).
  • fbs: WB supplies теперь type='fbs' (de3b1b1) — видны на «На сборке».
  • fbs: seller-warehouse filter не теряет legacy supplies + backfill endpoint (f1e021a).
  • fbs-sync: возврат safe reconciliation (a52e2f1) — auto-cancel stale draft orders.

Changed

  • EmptyState + pagination + 404/500 + mobile sidebar + auto-refresh (aaf1b2c).

Reverted

  • revert(fbs): архив FBS показывает только FBS, FBO остаётся отдельным разделом (8235fd4).

Как пополнять

  1. После каждого deploy — добавить запись в [Unreleased] под нужной секцией (Added/Changed/Fixed/Removed).
  2. Если изменение архитектурное — параллельно создать ADR в vibe-dev/knowledge/05-Decisions/YYYY-MM-DD-name.md и сослаться отсюда.
  3. При git tag/release — переместить [Unreleased] в новый раздел ## [vX.Y.Z] — YYYY-MM-DD и оставить пустой [Unreleased] сверху.
  4. Помощник: bash ops/deploy.sh после pm2 reload сам спросит про CHANGELOG и закоммитит вместе с кодом.

API Reference

{{COUNT_ROUTES}} endpoints из src/app/api/. JSDoc извлечён автоматически — где пусто, добавьте комментарий /** ... */ над export'ом.

/api/auth (16)

POST /api/auth/change-password 410 Gone — endpoint декомиссионирован 2026-05-11 (Batch-0 Fix 3).
410 Gone — endpoint декомиссионирован 2026-05-11 (Batch-0 Fix 3).

Раньше: проксировал credentials на `${NEXT_PUBLIC_API_URL}/api/auth-change-password/`
(apiinpacking.ru — отключён 2026-05-01). Если env переменная заполнена или
хост перерегистрировали — это credential leak на внешний host.

Текущий password change flow: `POST /api/wms/auth/change-password` или через
password reset flow (`/api/auth/reset-password`).
Файл: src/app/api/auth/change-password/route.ts
POST /api/auth/forgot-password body: { email }
POST /api/auth/forgot-password
body: { email }

Идемпотентно: всегда возвращает 200 (не палим существование email).
Если email есть и valid → создаём reset_token и шлём ссылку.
Файл: src/app/api/auth/forgot-password/route.ts
GET /api/auth/login (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/auth/login/route.ts
POST /api/auth/login 410 Gone — endpoint декомиссионирован 2026-05-11 (Batch-0 Fix 3).
410 Gone — endpoint декомиссионирован 2026-05-11 (Batch-0 Fix 3).

Раньше: проксировал credentials на `${NEXT_PUBLIC_API_URL}/api/auth-login/`
(apiinpacking.ru — отключён 2026-05-01). Если env переменная заполнена или
хост перерегистрировали — это credential leak на внешний host.

Текущий login flow: `POST /api/bcp/login` (локально на нашем Postgres).
Файл: src/app/api/auth/login/route.ts
GET /api/auth/oauth/[provider]/start OAuth-flow entry-point. Сейчас — placeholder, возвращает 501 до получения
GET /api/auth/oauth/[provider]/start

OAuth-flow entry-point. Сейчас — placeholder, возвращает 501 до получения
реальных WB Partner API / Ozon Seller / Telegram Widget credentials.

Когда credentials будут готовы:
 - wb:  redirect на WB Partner OAuth с client_id + redirect_uri
 - ozon: redirect на Ozon Seller Auth
 - telegram: redirect на Telegram Login Widget (или показать widget inline)

После OAuth callback'а — POST /api/auth/oauth/[provider]/callback
создаёт/находит platform_user по oauth_provider+oauth_provider_id,
затем заводит wms_user (orphan если новый) + session.
Файл: src/app/api/auth/oauth/[provider]/start/route.ts
POST /api/auth/reset-password body: { token, password }
POST /api/auth/reset-password
body: { token, password }

Использует one-shot token: помечает used_at чтобы повторно не сработал.
Обновляет password_hash в platform_user И wms_user (зеркало для login).
Файл: src/app/api/auth/reset-password/route.ts
GET /api/auth/seller/invites Возвращает active (pending) приглашения для email текущего залогиненного
GET /api/auth/seller/invites

Возвращает active (pending) приглашения для email текущего залогиненного
селлера. Используется в /seller dashboard inbox.
Файл: src/app/api/auth/seller/invites/route.ts
GET /api/auth/seller/me Возвращает профиль текущего залогиненного селлера + список его memberships
GET /api/auth/seller/me

Возвращает профиль текущего залогиненного селлера + список его memberships
(active wms_client rows в разных ФФ). Если не залогинен — 401.
Файл: src/app/api/auth/seller/me/route.ts
GET /api/auth/seller/notifications Возвращает список known event_types + текущие prefs пользователя
GET /api/auth/seller/notifications
Возвращает список known event_types + текущие prefs пользователя
(default = true для отсутствующих rows).
Файл: src/app/api/auth/seller/notifications/route.ts
PATCH /api/auth/seller/notifications body: { event_type, enabled }
PATCH /api/auth/seller/notifications
body: { event_type, enabled }
Файл: src/app/api/auth/seller/notifications/route.ts
POST /api/auth/seller/phone/verify/start Placeholder endpoint — full integration deferred до тех пор пока Telegram-bot
POST /api/auth/seller/phone/verify/start

Placeholder endpoint — full integration deferred до тех пор пока Telegram-bot
push pipeline (миграция кода в platform_user + delivery via @bcp_notifications)
не заработает end-to-end.

Сейчас возвращает 501 not_implemented если `BCP_PHONE_VERIFY_ENABLED` env
не установлен в `true`. Это явный сигнал клиенту что feature off.

Когда будет готов:
 1. Создать миграцию для phone_verify_codes (или поля в platform_user)
 2. Сохранять hash(code) + expires_at
 3. Если telegram_chat_id есть → POST в Telegram Bot API sendMessage
 4. Возвращать `has_telegram=true, code_ttl_seconds=600`
 5. Отдельный route /confirm — accept code, set phone_verified_at = NOW()
Файл: src/app/api/auth/seller/phone/verify/start/route.ts
POST /api/auth/seller/signup body: { email, password, phone, full_name?, agreement_accepted, oauth?: { provider, provider_id } }
POST /api/auth/seller/signup
body: { email, password, phone, full_name?, agreement_accepted, oauth?: { provider, provider_id } }

Self-registration селлера. Создаёт:
 - platform_user (is_seller=true, без is_platform_owner)
 - wms_user (orphan, ff_id=NULL) — нужен для wms_auth_session.user_id FK
 - session cookie → редирект на /seller (orphan dashboard)

Phone обязателен (для будущих Telegram/MAX-уведомлений), но СМС не шлём.
Phone verification — отдельным flow через Telegram bot.

OAuth (WB / Ozon / Telegram) — опционально, дополнительный способ входа.
Email+password всё равно обязательны.
Файл: src/app/api/auth/seller/signup/route.ts
POST /api/auth/seller/telegram/bind body: { chat_id: string }
POST /api/auth/seller/telegram/bind
body: { chat_id: string }

Привязывает telegram_chat_id к platform_user текущего seller'а
+ шлёт welcome-сообщение чтобы подтвердить что chat_id валидный.

Если Telegram возвращает 403 / 400 (user не запустил бота или blocked) —
binding отменяется и возвращается чёткая ошибка.

Auth: cookie auth_token. Юзер должен быть залогинен.
Файл: src/app/api/auth/seller/telegram/bind/route.ts
DELETE /api/auth/seller/telegram/bind (нет описания)
DELETE /api/auth/seller/telegram/bind — отвязать Telegram
Файл: src/app/api/auth/seller/telegram/bind/route.ts
POST /api/auth/seller/telegram/init Создаёт one-time bind-token и возвращает Telegram deep-link.
POST /api/auth/seller/telegram/init

Создаёт one-time bind-token и возвращает Telegram deep-link.
Юзер открывает t.me/bcp_ping_bot?start=<token>, Telegram → наш webhook
принимает /start <token>, accept'ит token, проставляет chat_id.

/seller frontend poll'ит /status?token=<t> → когда accepted_at заполнен →
редиректит и показывает success.
Файл: src/app/api/auth/seller/telegram/init/route.ts
GET /api/auth/seller/telegram/status Polling endpoint: фронт зовёт каждые 2 сек после init.
GET /api/auth/seller/telegram/status?token=<bind_token>

Polling endpoint: фронт зовёт каждые 2 сек после init.
Возвращает:
  - status: 'pending' | 'accepted' | 'expired' | 'not_found' | 'session_mismatch'
  - chat_id (только при accepted)
Файл: src/app/api/auth/seller/telegram/status/route.ts

/api/cron (6)

GET /api/cron/ai-health-check (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/cron/ai-health-check/route.ts
GET /api/cron/cleanup-expired-tokens Cron task: purges stale bind tokens from:
GET /api/cron/cleanup-expired-tokens

Cron task: purges stale bind tokens from:
  - wms_max_bind_token      (Wave 35, migration 113)
  - wms_telegram_bind_token (migration 080)

Deletion criteria (grace period = 24h, well above the 15-min TTL):
  - Rows that were already accepted/used AND accepted_at < NOW() - 24h
  - Rows that expired (expires_at < NOW() - 24h) regardless of use

wms_max_bind_token uses "accepted_at" for used flag.
wms_telegram_bind_token uses "accepted_at" as well (same schema shape).

Note: neither table has an ff_id column — bind tokens are global
(one platform_user → one MAX account, regardless of which FF the
user belongs to). Cleanup is therefore a global operation.

Auth: X-Cron-Secret header OR ?secret= query param.

Crontab (VM 102, to install after deploy):
  0 3 * * * curl -s -H "X-Cron-Secret: $WMS_CRON_SECRET" \
    http://localhost:7000/api/cron/cleanup-expired-tokens \
    >> /var/log/wms-cron-cleanup.log 2>&1

Returns: { ok: true, max_deleted: N, telegram_deleted: N }
Файл: src/app/api/cron/cleanup-expired-tokens/route.ts
GET /api/cron/fbo-proposal-reminder Cron-задача (запускается каждые 6 часов): для каждой FBO заявки с
GET /api/cron/fbo-proposal-reminder

Cron-задача (запускается каждые 6 часов): для каждой FBO заявки с
proposal_status='pending' и proposed_at > 24h ago — повторное напоминание
селлеру через notifyRequestSeller (Telegram + email fallback).

MVP анти-спам: окно [-30h, -24h] от NOW(). Заявки старше 30h уже выпали из
окна — селлер либо ответит, либо менеджер вручную cancel'нёт. (Полноценный
вариант требует колонку wms_request.proposal_reminded_at — добавим если
понадобится больше круга напоминаний.)

Auth: WMS_CRON_SECRET (query param ?secret или X-Cron-Secret header).

Return: { reminded, total }
Файл: src/app/api/cron/fbo-proposal-reminder/route.ts
GET /api/cron/fbo-wb-sync Cron-задача (запускается каждые 30 минут): для всех confirmed/in_progress
GET /api/cron/fbo-wb-sync

Cron-задача (запускается каждые 30 минут): для всех confirmed/in_progress
FBO supplies с marketplace='wb' и wb_binding_mode='api_pull' тянет
актуальный status из WB API.

Auth: X-Cron-Secret header (env WMS_CRON_SECRET).

Логика:
  - scanDt set + status='confirmed' → events log (no auto status change пока)
  - done=true → status='completed', completed_at=NOW()
  - Errors → log + continue (один селлер с invalid token не блокирует остальных)

Return: { synced, status_changes, errors[] }
Файл: src/app/api/cron/fbo-wb-sync/route.ts
GET /api/cron/storage-billing (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/cron/storage-billing/route.ts
POST /api/cron/storage-billing Простой cleanup — удаляет expired сессии. Сохранили для backward compat.
Простой cleanup — удаляет expired сессии. Сохранили для backward compat.

Auth: X-Cron-Secret header (preferred) ИЛИ ?secret=... query param (legacy).
Query param deprecated — попадает в access logs.
/
function verifyCronSecret(req: Request): boolean {
  const expected = process.env.WMS_CRON_SECRET;
  if (!expected) return false;
  const headerSecret = req.headers.get('x-cron-secret');
  // 🔒 SECURITY (Audit 2026-05-17 P1-2): убран legacy query-param fallback.
  // Query params попадают в nginx access logs plaintext'ом — leak vector.
  // Все cron jobs должны использовать `x-cron-secret` header (см. ops/cron-setup).
  return headerSecret === expected;
}

export async function GET(req: Request) {
  if (!verifyCronSecret(req)) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  }
  const sess = await wmsQuery<{ count: string }>(
    `WITH deleted AS (DELETE FROM wms_auth_session WHERE expires_at < NOW() RETURNING 1)
     SELECT COUNT(*)::text AS count FROM deleted`
  );
  return NextResponse.json({ ok: true, sessions_deleted: Number(sess[0].count) });
}

/**
POST /api/cron/storage-billing?period=YYYY-MM&secret=...

Ежемесячная генерация storage-invoice для всех клиентов всех FF.
Запускать 1-го числа за предыдущий период. Идемпотентно через
wms_storage_billing_log.UNIQUE (ff_id, period).
Файл: src/app/api/cron/storage-billing/route.ts

/api/health (1)

GET /api/health Используется UptimeRobot, мониторингом, пингами cron'ов.
GET /api/health

Используется UptimeRobot, мониторингом, пингами cron'ов.

Возвращает:
  - ok: bool
  - db: bool (пинг Postgres SELECT 1)
  - uptime_seconds: с момента pm2 worker'а
  - version: short SHA из ENV или 'dev'
  - migrations: count применённых миграций (число sql-файлов в /scripts/sql)

Не требует auth (публичный для healthcheck'еров).
Никогда не возвращает 500 — даже при ошибках БД отдаём 200 с db:false,
чтобы pm2 не убил worker; статус определяет внешний мониторинг по полю db.
Файл: src/app/api/health/route.ts

/api/platform (11)

POST /api/platform/billing/webhook/stripe (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/platform/billing/webhook/stripe/route.ts
GET /api/platform/invites (нет описания)
GET /api/platform/invites — список приглашений в текущий tenant
Файл: src/app/api/platform/invites/route.ts
POST /api/platform/invites Body: { email, role? }
POST /api/platform/invites
 Body: { email, role? }
 Создаёт invite-token, валиден 7 дней. URL = /invite/{token}
Файл: src/app/api/platform/invites/route.ts
GET /api/platform/invites/[token]/accept (нет описания)
GET /api/platform/invites/[token]/accept — детали инвайта (для UI accept-страницы)
Файл: src/app/api/platform/invites/[token]/accept/route.ts
POST /api/platform/invites/[token]/accept Rate-limit для invite accept (Batch-0 Fix 5 — 2026-05-11).
Rate-limit для invite accept (Batch-0 Fix 5 — 2026-05-11).
Утёкший invite token = silent takeover (auto-session). Лимит на (token + IP).

5 попыток за 10 минут → block 30 минут. Жёстче чем обычный login, потому что:
  - Каждый accept — single-use (token потом помечен accepted_at)
  - Атакующий не должен пробовать слабые пароли против утёкшего token
/
const INVITE_ACCEPT_LIMITS = {
  limit: 5,
  windowMs: 10 * 60 * 1000,
  blockMs: 30 * 60 * 1000,
};

/**
Password requirements для accept-flow.
Раньше: ≥4 символа — слабее чем signup (≥6) и индустриальная норма (≥8).
Теперь: ≥8 символов + смесь символов (мин. одна буква + одна цифра ИЛИ символ).
/
const AcceptBodySchema = z.object({
  full_name: z.string().trim().max(200).optional(),
  password: z.string()
    .min(8, 'Минимум 8 символов')
    .max(128, 'Максимум 128 символов')
    .refine(
      (p) => /[A-Za-zА-Яа-я]/.test(p) && /[0-9!@#$%^&*()_\-+=[\]{};:'",.<>/?\\|]/.test(p),
      'Должен содержать буквы и цифры/символы',
    ),
}).passthrough();

/** POST /api/platform/invites/[token]/accept
 Body: { full_name, password } — для нового юзера
       либо если юзер уже залогинен — auth_token достаётся из cookie/header
 Создаёт platform_user (если нет) + membership, возвращает auth-token.

 SECURITY (Batch-0 Fix 5 — 2026-05-11):
  - Rate-limit per (token + IP) — 5/10min → block 30min
  - Min password 8 chars + alnum complexity (zod)
  - Audit log: platform.invite.accepted с user_id + ff_id
  - Раньше: ≥4 char password, no rate-limit, no audit → silent takeover
Файл: src/app/api/platform/invites/[token]/accept/route.ts
GET /api/platform/settings (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/platform/settings/route.ts
PATCH /api/platform/settings (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/platform/settings/route.ts
POST /api/platform/signup body: { ff_name, full_name, email, password, plan? }
POST /api/platform/signup
body: { ff_name, full_name, email, password, plan? }

Регистрация нового ФФ. В одной транзакции:
  1. INSERT wms_ff (новый tenant)
  2. INSERT platform_user (owner с bcrypt-паролем)
  3. INSERT platform_membership (user × tenant, role='owner')
  4. INSERT platform_subscription для всех 4 модулей (wms+tasks+system+finance)
     на 14-дневный trial
  5. INSERT wms_user (зеркало в WMS, чтобы login через /api/bcp/auth/login работал)
  6. createSession() — возвращаем токен в cookie

Идемпотентность: если email уже зарегистрирован → 409.
Файл: src/app/api/platform/signup/route.ts
GET /api/platform/tenants Возвращает список доступных компаний для текущего юзера:
GET /api/platform/tenants

Возвращает список доступных компаний для текущего юзера:
  - Для платформенного owner — все wms_ff
  - Для обычного юзера — только те где у него есть membership
  - Маркер is_current — какой сейчас активный (из active_tenant cookie)
Файл: src/app/api/platform/tenants/route.ts
POST /api/platform/tenants Body: { tenant_id }
POST /api/platform/tenants/switch
Body: { tenant_id }

Устанавливает cookie active_tenant. Юзер должен быть platform_owner
либо иметь membership к этому tenant'у.
Файл: src/app/api/platform/tenants/route.ts
POST /api/platform/tenants/create body: { ff_name, plan? }
POST /api/platform/tenants/create
body: { ff_name, plan? }

Создание дополнительного ФФ-tenant'а для УЖЕ авторизованного platform_user.
Это нативный multi-tenant сценарий: один email = много ФФ под управлением.

В отличие от /api/platform/signup (который создаёт NEW platform_user
для свежей регистрации) — этот endpoint требует валидную auth-сессию
и привязывает новый ФФ к существующему platform_user.

В одной транзакции:
  1. INSERT wms_ff (новый tenant) с 14-дневным trial
  2. INSERT platform_membership (existing user × new tenant, role='owner')
  3. INSERT 4 platform_subscription (wms+tasks+system+finance, trial 14d)
  4. INSERT wms_user (зеркало текущего юзера в новом ФФ)
  5. INSERT wms_warehouse 'Основной склад'

После успеха клиент может вызвать POST /api/platform/tenants со switch
чтобы переключиться на новый tenant.
Файл: src/app/api/platform/tenants/create/route.ts

/api/telegram (1)

POST /api/telegram/webhook Telegram Update receiver. Use either webhook OR polling — но не оба.
POST /api/telegram/webhook

Telegram Update receiver. Use either webhook OR polling — но не оба.
NB: на этой инфраструктуре webhook timeout'ит от Telegram серверов (NAT
issue) — используем polling через /api/cron/telegram-poll.

Webhook endpoint оставлен на случай если в будущем поменяется infra.
Защищён secret-token header.
Файл: src/app/api/telegram/webhook/route.ts

/api/tsd (4)

POST /api/tsd/auth/session body: { pin, device_id?, device_name?, app_version? }
POST /api/tsd/auth/session
body: { pin, device_id?, device_name?, app_version? }

TSD session creator: resolve user по PIN → create session token.
Token используется в X-TSD-Token header для всех остальных /api/tsd/* запросов.

Public endpoint (uses PIN auth) — добавляется в middleware allowlist.

SECURITY (важно):
  - PIN сравнивается через bcrypt против `wms_user.pin_hash` (миграция 053).
    Раньше query был `WHERE pin = $1` plaintext, но миграция 053 (2026-05-07)
    обнулила колонку `pin` — TSD endpoint был сломан до этого фикса.
  - Бэкенд fetch'ит все активные user'ы с непустым pin_hash и bcrypt-verify
    по очереди. Для типичного tenant'а ~5-20 операторов → 50-200 ms на
    unauthenticated PIN-fail. Защищён rate-limit'ом (5 fails/min).
  - Коллизия PIN'ов между tenant'ами → `ambiguous_pin` 409. Admin должен
    назначать уникальные PIN'ы внутри tenant'а; cross-tenant collision
    неприемлема (TSD без email).
Файл: src/app/api/tsd/auth/session/route.ts
DELETE /api/tsd/auth/session Headers: X-TSD-Token: ...
DELETE /api/tsd/auth/session
Headers: X-TSD-Token: ...

Завершает сессию TSD.
Файл: src/app/api/tsd/auth/session/route.ts
POST /api/tsd/move body: { product_id, qty, type: 'put_away'|'pick'|'transfer'|'adjust',
POST /api/tsd/move
body: { product_id, qty, type: 'put_away'|'pick'|'transfer'|'adjust',
        from_cell_id?, to_cell_id?, box_id?, notes? }
Headers: X-TSD-Token: ...

Универсальный создатель movement из ТСД с авто bundle-disassembly.

Для kладовщика на сканере: pick → отгрузка, put_away → размещение,
transfer → перенос между ячейками, adjust → корректировка.
Файл: src/app/api/tsd/move/route.ts
POST /api/tsd/scan/resolve body: { code }
POST /api/tsd/scan/resolve
body: { code }
Headers: X-TSD-Token: ...

Универсальный резолвер штрихкодов для ТСД:
  - Если код = sku/barcode product → возвращает товар + остатки
  - Если код = адрес ячейки (A-1-2-3) → возвращает cell + содержимое
  - Если код = № коробки (SK-...) → возвращает box + товары в нём
  - Если код = DataMatrix КИЗ → возвращает kiz + товар

Используется ТСД для quick-lookup при сканировании.
Файл: src/app/api/tsd/scan/resolve/route.ts

/api/webhooks (1)

POST /api/webhooks/max/[ff_id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/webhooks/max/[ff_id]/route.ts

/api/wms (368)

GET /api/wms/account/link-max (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/account/link-max/route.ts
POST /api/wms/account/link-max (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/account/link-max/route.ts
DELETE /api/wms/account/link-max (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/account/link-max/route.ts
GET /api/wms/account/preferred-locale Sets the calling user's preferred notification locale.
PATCH /api/wms/account/preferred-locale

Sets the calling user's preferred notification locale.
Stored on platform_user.preferred_locale (added in migration 115).
NULL is accepted to reset to tenant default.

Wave 38 — i18n for server-side notifications.
/

const VALID_LOCALES = ['ru', 'en', 'uz', 'kz'] as const;

const BodySchema = z.object({
  locale: z.enum(VALID_LOCALES).nullable(),
});

export async function PATCH(req: Request) {
  try {
    const actorId = await getActorId(req);

    const raw = await req.json().catch(() => ({}));
    const parsed = BodySchema.safeParse(raw);
    if (!parsed.success) {
      return NextResponse.json(
        { error: 'validation', issues: parsed.error.issues },
        { status: 400 },
      );
    }

    const { locale } = parsed.data;

    // Resolve platform_user_id for this wms_user
    const [row] = await wmsQuery<{ platform_user_id: string | null }>(
      `SELECT platform_user_id::text FROM wms_user WHERE id = $1 AND status = 'active' LIMIT 1`,
      [actorId],
    );

    if (!row || !row.platform_user_id) {
      return apiError(req, {
        error: 'platform_user_not_found',
        messageKey: 'errors.platform_user_not_found',
        fallback: 'User profile not found',
        status: 404,
      });
    }

    await wmsQuery(
      `UPDATE platform_user
          SET preferred_locale = $1,
              updated_at = NOW()
        WHERE id = $2`,
      [locale, row.platform_user_id],
    );

    return NextResponse.json({ ok: true, preferred_locale: locale });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return NextResponse.json({ error: 'update_failed', message }, { status: 500 });
  }
}

/**
GET /api/wms/account/preferred-locale

Returns the calling user's current preferred_locale.
Файл: src/app/api/wms/account/preferred-locale/route.ts
PATCH /api/wms/account/preferred-locale (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/account/preferred-locale/route.ts
GET /api/wms/admin/audit Унифицированный audit-log из wms_request_event.
GET /api/wms/admin/audit?from=&to=&user_id=&request_id=&event_type=

Унифицированный audit-log из wms_request_event.
Доступ: только platform_owner или роль admin.

Возвращает 200 последних событий по фильтру.
Файл: src/app/api/wms/admin/audit/route.ts
GET /api/wms/admin/audit-log ?action=product.update — точное совпадение
GET /api/wms/admin/audit-log
  ?action=product.update              — точное совпадение
  &entity_table=wms_product           — фильтр по таблице
  &entity_id=42                       — фильтр по конкретной записи
  &actor_id=5                         — кто действовал
  &from=2026-05-01&to=2026-05-08      — период
  &search=канистра                    — поиск по summary
  &limit=200                          — pagination
  &offset=0

Read-only. Доступ: только owner / platform_owner текущего ФФ.

Источник — wms_audit_log (B4 hybrid scope, добавлено 2026-05-08).
Для событий заявок — отдельный endpoint /api/wms/admin/audit (wms_request_event).
Файл: src/app/api/wms/admin/audit-log/route.ts
GET /api/wms/admin/cron-runs Authoritative list всех cron tasks через wms_cron_run (migration 070).
GET /api/wms/admin/cron-runs

Authoritative list всех cron tasks через wms_cron_run (migration 070).
Возвращает row per slug + computed health (ok / stale / failed).

Каждый cron task должен UPSERT'ить в wms_cron_run при invocation
(см. cronRoute() helper). Если cron не записан — считается never_ran.

Доступно только platform_owner.
Файл: src/app/api/wms/admin/cron-runs/route.ts
GET /api/wms/admin/health Сводка здоровья системы для ФФ-админа:
GET /api/wms/admin/health

Сводка здоровья системы для ФФ-админа:
  - Статус БД и количество строк по ключевым таблицам
  - Последние записи журнала хранения (когда последний раз cron работал)
  - Последние записи WB-sync (если table'ы существуют)
  - Последние backups (по размеру log)
  - Активные cron'ы (мы знаем какие должны быть, проверяем по последним меткам)

Доступно только platform_owner.
Файл: src/app/api/wms/admin/health/route.ts
GET /api/wms/admin/max-bot-config Returns public info about the MAX bot configured for the current tenant.
GET /api/wms/admin/max-bot-config
Returns public info about the MAX bot configured for the current tenant.
Never returns the token itself.

Response: { configured: boolean, bot_username: string | null, set_at: string | null, has_platform_fallback: boolean }
Файл: src/app/api/wms/admin/max-bot-config/route.ts
PUT /api/wms/admin/max-bot-config body: { bot_token: string, bot_username?: string }
PUT /api/wms/admin/max-bot-config
body: { bot_token: string, bot_username?: string }

Store/replace the MAX bot token for the current tenant.
Token is validated via MAX API getMe, then encrypted at rest.
Owner/admin only.
Файл: src/app/api/wms/admin/max-bot-config/route.ts
DELETE /api/wms/admin/max-bot-config Clear the custom MAX bot token — revert to platform fallback (if any).
DELETE /api/wms/admin/max-bot-config
Clear the custom MAX bot token — revert to platform fallback (if any).
Owner/admin only.
Файл: src/app/api/wms/admin/max-bot-config/route.ts
GET /api/wms/admin/notifications Audit endpoint для админа ФФ — последние notification attempts.
GET /api/wms/admin/notifications?status=&event=&from=&to=&limit=100

Audit endpoint для админа ФФ — последние notification attempts.
Filters: status, event_type, date range.

Доступно: platform_owner.
Файл: src/app/api/wms/admin/notifications/route.ts
POST /api/wms/ai/chat body: { messages: [{role, content}], temperature?, max_tokens? }
POST /api/wms/ai/chat
body: { messages: [{role, content}], temperature?, max_tokens? }

Generic chat completion endpoint. Использует gpt-5.5 через ChatGPT Pro proxy
(AI-VPS CLIProxyAPI Docker) с fallback на OpenAI direct API.

Все вызовы пишутся в wms_ai_call_log (audit + cost tracking).
Файл: src/app/api/wms/ai/chat/route.ts
POST /api/wms/ai/extract-pdf Resolved (если SKU найден в нашем каталоге) */
Resolved (если SKU найден в нашем каталоге) */
  product_id?: string;
  /** Notes от AI: «не найдено», «найдено через name fuzzy» */
  match_note?: string;
}

interface ExtractResult {
  document_type: 'invoice' | 'delivery_note' | 'receipt' | 'unknown';
  supplier_name?: string;
  supplier_inn?: string;
  document_number?: string;
  document_date?: string;
  total?: number;
  items: ExtractedItem[];
  warnings?: string[];
}

/**
POST /api/wms/ai/extract-pdf
multipart/form-data: file=<pdf>, client_id?

AI-парсер накладной от поставщика для приёмки. Принимает PDF → извлекает
текст через pdf-parse → отправляет в gpt-5.5 для извлечения структурированных
данных → резолвит SKU в нашем каталоге → возвращает preview JSON.

Оператор смотрит preview, корректирует, потом через UI создаёт заявку.
Этот endpoint НЕ создаёт wms_request — только extracts данные.

Improvement над ручным вводом: оператор экономит 5-10 минут на каждую
приёмку (раньше: открыть PDF, найти SKU, вбить qty одной рукой).
Файл: src/app/api/wms/ai/extract-pdf/route.ts
POST /api/wms/ai/products/[id]/categorize Keywords для поиска */
Keywords для поиска */
  keywords: string[];
  /** Уверенность в категоризации */
  confidence: 'high' | 'medium' | 'low';
  /** Reasoning — что повлияло на категорию */
  reasoning: string;
}

/**
POST /api/wms/ai/products/[id]/categorize
body: { apply?: boolean } — если true, сохраняет результат в wms_product

AI-категоризация товара. Берёт name + barcode + supplier_name + текущую
category и предлагает структурированные значения category/subcategory/
color/size_label/keywords.

Improvement over manual: ребята не любят заполнять category вручную;
AI делает это в 1 клик по name+description.
Файл: src/app/api/wms/ai/products/[id]/categorize/route.ts
POST /api/wms/ai/products/[id]/forecast body: { days_history?: number = 90, target_stock_days?: number = 14 }
POST /api/wms/ai/products/[id]/forecast
body: { days_history?: number = 90, target_stock_days?: number = 14 }

AI-прогноз когда кончится товар + рекомендация по reorder.

Логика:
  1. Берём movements за days_history (по умолчанию 90 дней)
  2. Аггрегируем shipment (ship/pick) по дням
  3. Отправляем series в gpt-5.5 → анализ trend + прогноз
  4. Текущий остаток / daily_avg = days_to_stockout
  5. recommended_reorder_qty = daily_avg * target_stock_days

Use-case для оператора склада: при reorder поставщику видишь когда
критично, сколько заказать.
Файл: src/app/api/wms/ai/products/[id]/forecast/route.ts
POST /api/wms/ai/products/bulk-categorize Список product_ids; ИЛИ {only_missing_category: true} — все товары без category */
Список product_ids; ИЛИ {only_missing_category: true} — все товары без category */
  product_ids?: string[];
  only_missing_category?: boolean;
  /** Применить результат к БД или вернуть suggestions */
  apply?: boolean;
  /** Max products to process в одном request — защита от долгих run'ов */
  limit?: number;
}

interface ProductForCat {
  id: string;
  sku: string;
  name: string;
  barcode: string | null;
  current_category: string | null;
  supplier: string | null;
}

interface BatchResult {
  id: string;
  category: string;
  subcategory?: string;
  color?: string;
  size_label?: string;
  confidence: 'high' | 'medium' | 'low';
}

/**
POST /api/wms/ai/products/bulk-categorize

Batch-категоризация: одним LLM-вызовом обрабатываем до 30 товаров.
Эффективность: вместо 30 отдельных запросов — 1 на 30 товаров (10x экономия
tokens на system prompt).
Файл: src/app/api/wms/ai/products/bulk-categorize/route.ts
POST /api/wms/ai/requests/[id]/summarize body: { language?: 'ru'|'en' }
POST /api/wms/ai/requests/[id]/summarize
body: { language?: 'ru'|'en' }

AI-резюме заявки: что произошло, текущее состояние, риски, рекомендации.
Для клиента (понятным языком) или для оператора (с действиями).
Файл: src/app/api/wms/ai/requests/[id]/summarize/route.ts
GET /api/wms/ai/status Возвращает состояние AI-провайдеров (какие env настроены) + usage stats
GET /api/wms/ai/status

Возвращает состояние AI-провайдеров (какие env настроены) + usage stats
за последние 7 дней. Полезно для admin/debug.

Маскирует ключи — отображает только presence (true/false), не значения.
Файл: src/app/api/wms/ai/status/route.ts
GET /api/wms/ai/stock-anomalies Wave 22 — Anomaly detection в stock movements.
GET /api/wms/ai/stock-anomalies

Wave 22 — Anomaly detection в stock movements.

Логика:
  1. Берём movements за последние 30 дней per product (тенанта)
  2. Считаем mean + std dev qty
  3. Outliers: |qty - mean| > 3σ (rule of three sigma)
  4. Для top-10 outliers — AI explanation (cost / impact analysis)

Use case: оператор / owner видит подозрительные движения (большая запись
в один момент, регулярные late-night transfers, выход за норму).

Multi-tenant: каждый product принадлежит client, client → ff_id (через JOIN).
Файл: src/app/api/wms/ai/stock-anomalies/route.ts
POST /api/wms/ai/suggest Optional CTA-link */
Optional CTA-link */
  action_href?: string;
  action_label?: string;
  /** Numeric / data hints */
  metrics?: Record<string, number | string>;
}

/**
POST /api/wms/ai/suggest
body: { context: 'dashboard' | 'product' | 'request' | 'stage' | 'inventory', entity_id?: string }

AI-assistant suggestions для оператора. Текущая реализация — rule-based
heuristics на реальных данных (БД-стат): low stock alerts, overdue requests,
inactive products, missing tariffs.

Архитектура: rule-based за основу + extension point для LLM-integration
(через ChatGPT Pro proxy на AI-VPS, см. CLAUDE.md). При появлении LLM
стратегии — этот endpoint расширим.
Файл: src/app/api/wms/ai/suggest/route.ts
GET /api/wms/analytics Composite analytics endpoint для FF dashboard. Возвращает 4 секции:
GET /api/wms/analytics

Composite analytics endpoint для FF dashboard. Возвращает 4 секции:

 - top_clients: top-10 клиентов по обороту за last 30d (FBS+DBS+inbound completed)
 - debtors: client'ы с unpaid invoices >7 дней
 - sku_turnover: top-10 по обороту (units shipped / units in stock), low turnover first
 - warehouse_load: текущая заполненность + прогноз на 7/30 дней

Одним запросом — для дашборда страницы (4 widgets в одном fetch).
Файл: src/app/api/wms/analytics/route.ts
GET /api/wms/anomaly/list Query:
GET /api/wms/anomaly/list — список открытых аномалий для оператора.

Query:
  ?for_me=1     — только мои (user_id = current_user)
  ?status=open  — фильтр по статусу
  ?limit=20

Multi-tenant: фильтр по ff_id из getResolvedTenant().
Файл: src/app/api/wms/anomaly/list/route.ts
GET /api/wms/app/latest Возвращает самую свежую версию wms-tsd APK для текущего ff_id.
GET /api/wms/app/latest

Возвращает самую свежую версию wms-tsd APK для текущего ff_id.
Используется TSD приложением при старте + WorkManager каждые 6h для проверки OTA.

Логика:
  1. Сначала ищем релиз с конкретным ff_id текущего тенанта (custom build для tenant'а).
  2. Если нет — fallback на глобальный релиз (ff_id IS NULL).
  3. Берём с наибольшим version_code.

Response shape (success):
  {
    version_code: 46,
    version_name: "0.15.0",
    download_url: "https://bcp.vibe-dev.team/downloads/wms-tsd-0.15.0.apk",
    sha256: "ab12...",
    file_size: 12345678,
    changelog: "## 0.15.0\n- OTA updates\n- KIZ scanner",
    mandatory: false,
    min_supported_version: 30,
    published_at: "2026-05-16T04:00:00Z"
  }

Если ни одного релиза нет — 404 { error: 'no_releases' }.
Файл: src/app/api/wms/app/latest/route.ts
POST /api/wms/app/register (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/app/register/route.ts
GET /api/wms/audit/search Search/filter audit log. Возвращает paginated результат + facets для UI:
GET /api/wms/audit/search?action=&entity=&user=&from=&to=&q=&page=&per_page=

Search/filter audit log. Возвращает paginated результат + facets для UI:
  - actions: список уникальных action в результате (для filter)
  - entities: список уникальных entity_table

Multi-tenant: ВСЕГДА фильтрует по ff_id.
Файл: src/app/api/wms/audit/search/route.ts
POST /api/wms/auth/2fa/disable body: { password }
POST /api/wms/auth/2fa/disable
body: { password }

Отключает 2FA. Требует подтверждения паролем чтобы атакующий с украденной
сессией не мог отключить 2FA.
Файл: src/app/api/wms/auth/2fa/disable/route.ts
POST /api/wms/auth/2fa/enable body: { secret, code }
POST /api/wms/auth/2fa/enable
body: { secret, code }

Подтверждение setup: проверяет TOTP-код от текущего secret, и если OK —
сохраняет secret + генерит 10 backup-кодов (хешированными).

Возвращает: { ok, backup_codes: [...] } — backup-коды показываются один раз.
Файл: src/app/api/wms/auth/2fa/enable/route.ts
POST /api/wms/auth/2fa/setup Генерит новый TOTP secret + QR-data-URL для отображения в UI.
POST /api/wms/auth/2fa/setup

Генерит новый TOTP secret + QR-data-URL для отображения в UI.
НЕ сохраняет в БД — это произойдёт в /enable когда юзер подтвердит первый код.

Возвращает:
  { secret, otpauth_url, qr_data_url }
Файл: src/app/api/wms/auth/2fa/setup/route.ts
GET /api/wms/auth/2fa/status Возвращает: { enabled, enabled_at, backup_remaining }
GET /api/wms/auth/2fa/status
Возвращает: { enabled, enabled_at, backup_remaining }
Файл: src/app/api/wms/auth/2fa/status/route.ts
POST /api/wms/auth/2fa/verify body: { code }
POST /api/wms/auth/2fa/verify
body: { code }

Проверяет TOTP-код или backup-код для текущего юзера. Используется для:
 - первоначального login (после verifyPassword) — сейчас не привязано
 - sensitive actions (например, перед отключением 2FA — лучше отдельная
   проверка пароля + код)
 - тестирования что юзер сохранил аутентификатор корректно

Если использован backup-код — он удаляется из массива (single-use).
Файл: src/app/api/wms/auth/2fa/verify/route.ts
GET /api/wms/auth/me Возвращает текущего user'а по `X-Auth-Token` (или cookie `auth_token`).
GET /api/wms/auth/me

Возвращает текущего user'а по `X-Auth-Token` (или cookie `auth_token`).
Используется и web-клиентом, и ТСД-приложением для:
  - валидации токена при холодном старте
  - получения свежих user-данных (full_name, position, owner)

Shape ответа полностью совпадает с user-частью /pin-login (без token):
  { user: { id, email, full_name, position, owner: { type, id } } }

Также bumps last_seen_at в wms_auth_session — для мониторинга активности.
Файл: src/app/api/wms/auth/me/route.ts
POST /api/wms/auth/pin-login Облегчённая авторизация для ТСД (Терминал Сбора Данных) и mobile-устройств:
POST /api/wms/auth/pin-login

Облегчённая авторизация для ТСД (Терминал Сбора Данных) и mobile-устройств:
вместо email+password юзер вводит 4-6 значный PIN (быстрее, удобнее на
портативных сканерах с numeric keypad).

Body: { email: string, pin: string }
  email   — обычный wms_user.email
  pin     — 4-6 цифр, верифицируется bcrypt'ом против wms_user.pin_hash

Вернёт ту же сессию что обычный /auth/login (token + user) — фронт
использует один и тот же flow дальше.

SECURITY:
  - PIN — короткий, brute-force-friendly. Поэтому:
    * Хешируется bcrypt (cost 10)
    * NOT-FOUND и WRONG-PIN дают одинаковый response → не leak'аем существование email'а
    * Rate-limit: 5 попыток / 60 сек / IP-email pair, блок 5 минут.
      Persistent через wms_app_state['ratelimit:pin:<ip>:<email>']
      (миграция 091) — pm2 cluster=8 share один counter, не in-memory.
      4-значный PIN cracked в 10K попытках → с lock'ом 5min: 14 days
      worst-case при naive scan one-per-block. Достаточно для ТСД usage.
  - 2FA не применяется — ТСД физически в руках работника
Файл: src/app/api/wms/auth/pin-login/route.ts
POST /api/wms/auth/set-pin Устанавливает PIN для текущего пользователя (или для указанного user_id —
POST /api/wms/auth/set-pin

Устанавливает PIN для текущего пользователя (или для указанного user_id —
если current user — owner / админ).

Body: { pin: string, user_id?: string }
  pin       — 4-8 цифр (для удобного ввода на ТСД)
  user_id   — (опционально) если меняем чужой PIN; требует role=owner

Если pin = "" или null — сбрасывает PIN (запрет на pin-login).
Файл: src/app/api/wms/auth/set-pin/route.ts
GET /api/wms/barcode Универсальный barcode/datamatrix renderer на основе bwip-js.
GET /api/wms/barcode?bcid=code128&text=...&scale=2&height=14

Универсальный barcode/datamatrix renderer на основе bwip-js.
Возвращает PNG, кешируется на 1 час (по тексту коды детерминированы).

Полезные bcid:
  - code128         — обычные штрих-коды (артикулы, коробки)
  - datamatrix      — generic DataMatrix
  - **gs1datamatrix** — GS1 DataMatrix с FNC1 (КИЗ Честный Знак, ПП РФ №792).
    Текст содержит AI-разделители: (01)<gtin>(21)<serial>(91)<tnved>(92)<crypto>
  - ean13 / ean8    — потребительские товары
  - qrcode          — ссылки/QR

TD-013 (2026-05-15): добавлен явный bcid=gs1datamatrix для КИЗ. bwip-js
сам инжектирует FNC1 и валидирует структуру AI-codes.
Файл: src/app/api/wms/barcode/route.ts
GET /api/wms/billing/reconciliation Акт сверки за период:
GET /api/wms/billing/reconciliation?client_id=&from=&to=

Акт сверки за период:
  - Все invoices клиента созданные в период
  - Все payments клиента за период
  - Балансы: открытые / оплаченные / разница

Если to не задан — конец текущего месяца. Если from — начало.
Файл: src/app/api/wms/billing/reconciliation/route.ts
GET /api/wms/billing/storage (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/billing/storage/route.ts
POST /api/wms/billing/storage (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/billing/storage/route.ts
GET /api/wms/boxes (нет описания)
GET /api/wms/boxes?search=&barcode=&client_id=&page=&per_page=
Файл: src/app/api/wms/boxes/route.ts
POST /api/wms/boxes body: { code?, barcode?, client_id?, current_cell_id?, weight_kg?, notes? }
POST /api/wms/boxes
body: { code?, barcode?, client_id?, current_cell_id?, weight_kg?, notes? }
Если code не указан — генерируется автоматически BOX-YYYY-NNNNNN.
Файл: src/app/api/wms/boxes/route.ts
GET /api/wms/boxes/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/boxes/[id]/route.ts
PATCH /api/wms/boxes/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/boxes/[id]/route.ts
DELETE /api/wms/boxes/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/boxes/[id]/route.ts
POST /api/wms/boxes/[id]/items body: { product_id, qty } — добавить/обновить товар в коробке
POST /api/wms/boxes/[id]/items
body: { product_id, qty }   — добавить/обновить товар в коробке

Идемпотентно (UPSERT по box+product). Для увеличения количества вызывайте повторно.
Файл: src/app/api/wms/boxes/[id]/items/route.ts
DELETE /api/wms/boxes/[id]/items (нет описания)
DELETE /api/wms/boxes/[id]/items?item_id=X — удалить позицию из коробки
Файл: src/app/api/wms/boxes/[id]/items/route.ts
POST /api/wms/boxes/[id]/move body: { cell_id: string|null, cell_address?: string }
POST /api/wms/boxes/[id]/move
body: { cell_id: string|null, cell_address?: string }

Перемещает коробку в указанную ячейку. cell_id или cell_address — одно из.
Создаёт transfer-movement(ы) для всех товаров в коробке (from old → to new).
Обновляет:
 - wms_box.current_cell_id = new
 - wms_cell.status = 'occupied' (new), 'free' (old, если коробок там больше нет)
Файл: src/app/api/wms/boxes/[id]/move/route.ts
GET /api/wms/bundles Список товаров-комплектов (is_bundle=true) с количеством компонент и
GET /api/wms/bundles
Список товаров-комплектов (is_bundle=true) с количеством компонент и
прогнозом сколько комплектов мы можем собрать прямо сейчас.
Файл: src/app/api/wms/bundles/route.ts
POST /api/wms/bundles body: { product_id, components: [{component_product_id, qty}], assembly_cost?, min_assembly_qty?, assembly_notes? }
POST /api/wms/bundles
body: { product_id, components: [{component_product_id, qty}], assembly_cost?, min_assembly_qty?, assembly_notes? }
Помечает товар как bundle и устанавливает компоненты атомарно.
Файл: src/app/api/wms/bundles/route.ts
GET /api/wms/bundles/[id] Сколько мы можем собрать комплектов из ЭТОГО компонента */
Сколько мы можем собрать комплектов из ЭТОГО компонента */
  max_assemblable_from_this: number;
}

interface BundleDetail {
  id: string;
  sku: string;
  name: string;
  client_id: string | null;
  client_name: string | null;
  assembly_cost: string;
  min_assembly_qty: number;
  assembly_notes: string | null;
  components: ComponentRow[];
  max_assemblable: number;
}

/**
GET /api/wms/bundles/[id]
Bundle details с разбивкой по компонентам и stock availability.
Файл: src/app/api/wms/bundles/[id]/route.ts
DELETE /api/wms/bundles/[id] Снимает флаг is_bundle и удаляет компоненты (товар остаётся).
DELETE /api/wms/bundles/[id]
Снимает флаг is_bundle и удаляет компоненты (товар остаётся).
Файл: src/app/api/wms/bundles/[id]/route.ts
GET /api/wms/cabinet/changelog (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/cabinet/changelog/route.ts
GET /api/wms/cabinet/storage Аналитика хранения для клиентского кабинета:
GET /api/wms/cabinet/storage?from=YYYY-MM-DD&to=YYYY-MM-DD[&client_id=]

Аналитика хранения для клиентского кабинета:
  - Daily breakdown: дата → объём (м³) + cost (₽) из wms_storage_log
  - Top SKU по доле объёма (через wms_stock current snapshot)
  - Total: сумма + дни + средний дневной cost
  - Paid invoices: список «storage_period» invoices в периоде

Если юзер — клиент ФФ (wms_user.client_id NOT NULL) → автоматически только
его данные. Если ФФ-админ — может указать client_id (для просмотра конкретного).

Default период: текущий месяц.
Файл: src/app/api/wms/cabinet/storage/route.ts
POST /api/wms/calculator/quote body: { items: [{service_id, qty}], client_id?: string }
POST /api/wms/calculator/quote
body: { items: [{service_id, qty}], client_id?: string }

Возвращает быструю смету с применением:
  - default_price из wms_service_catalog
  - override per-client из wms_client_tariff

Quick-quote — без сохранения, для предложения клиенту/расчёта.
Улучшение над SkladBot: возвращаем breakdown с has_client_override
чтобы видеть какие услуги пересчитаны под клиента.
Файл: src/app/api/wms/calculator/quote/route.ts
GET /api/wms/cells Список ячеек с фильтрацией.
GET /api/wms/cells?warehouse_id=&stillage=&address=&status=&type=
Список ячеек с фильтрацией.

Используется:
 - /wms/warehouses/[id] — список ячеек склада
 - Сценарий «scan cell» — найти ячейку по address для перемещения коробки
Файл: src/app/api/wms/cells/route.ts
GET /api/wms/cells/[id] (нет описания)
GET /api/wms/cells/[id] — детали ячейки + список коробок в ней.
Файл: src/app/api/wms/cells/[id]/route.ts
POST /api/wms/cells/reorder body: { cell_orders: [{cell_id, position}, ...] }
POST /api/wms/cells/reorder
body: { cell_orders: [{cell_id, position}, ...] }

Bulk update position of cells. Используется в visual stellage editor
для drag-drop reorder ячеек внутри этажа.

Multi-tenant: проверяем что все cells принадлежат tenant'у через JOIN.
Файл: src/app/api/wms/cells/reorder/route.ts
GET /api/wms/cells/suggest Подсказка ячейки для приёмки:
GET /api/wms/cells/suggest?product_id=&warehouse_id=&limit=5

Подсказка ячейки для приёмки:
  1. Сначала ячейки, где этот product уже лежит (consolidation,
     derived из wms_movement to_cell_id агрегации)
  2. Затем ячейки status='free' в том же warehouse (раскладка с нуля)

Используется при приёмке: приёмщик сканит товар → видит «положи в A-1-3-2,
там уже 5 шт. этого товара» либо «свободная ячейка B-2-1-1».
Файл: src/app/api/wms/cells/suggest/route.ts
GET /api/wms/clients (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/route.ts
POST /api/wms/clients (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/route.ts
GET /api/wms/clients/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/[id]/route.ts
PATCH /api/wms/clients/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/[id]/route.ts
DELETE /api/wms/clients/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/[id]/route.ts
POST /api/wms/clients/[id]/invite body: { email?, expires_in_days? (default 14) }
POST /api/wms/clients/[id]/invite
body: { email?, expires_in_days? (default 14) }

Создаёт invite для EXISTING wms_client (карточка уже создана ФФ вручную,
нужно дать селлеру доступ к ней). При accept — wms_client.platform_user_id
проставится FK.

email опционален — если есть в wms_client.email, берём оттуда.

Доступ: только staff ФФ.
Файл: src/app/api/wms/clients/[id]/invite/route.ts
POST /api/wms/clients/bulk-import multipart/form-data: file=<xlsx/csv>
POST /api/wms/clients/bulk-import

multipart/form-data: file=<xlsx/csv>

Excel columns:
  name* | inn | phone | email | director_name | address | telegram_chat_id

Strategy: upsert по (ff_id, inn) если есть, иначе по (ff_id, name).
Возвращает preview с action='create'|'update'|'skip'.

BLOCKER #1 для онбординга — клиент новый ФФ должен импортить базу селлеров
из 1С / Excel за один раз.
Файл: src/app/api/wms/clients/bulk-import/route.ts
POST /api/wms/clients/bulk-import/[id]/apply body: { skip_failed?: boolean, mode?: 'create_only' | 'update_only' | 'both' }
POST /api/wms/clients/bulk-import/[id]/apply
body: { skip_failed?: boolean, mode?: 'create_only' | 'update_only' | 'both' }

Применяет clients bulk-import preview.
Файл: src/app/api/wms/clients/bulk-import/[id]/apply/route.ts
POST /api/wms/clients/bulk-invite CSV формат:
POST /api/wms/clients/bulk-invite (text/csv)

CSV формат:
  name,inn,email,phone
  "ИП Иванов","772233445566","ivanov@x.ru","+79991234567"

Логика:
 - INSERT wms_client (если ИНН ещё не зарегистрирован → skip)
 - INSERT platform_invite с token (для регистрации селлера)
 - Send email со ссылкой

Возвращает: { created, skipped, invites_sent, errors[] }
Файл: src/app/api/wms/clients/bulk-invite/route.ts
POST /api/wms/clients/import (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/clients/import/route.ts
GET /api/wms/clients/invite/[token] Public preview — возвращает info об invite чтобы UI мог:
GET /api/wms/clients/invite/[token]

Public preview — возвращает info об invite чтобы UI мог:
 - Показать имя ФФ который пригласил
 - Pre-fill email/name на signup form
 - Показать expired/revoked status

НЕ требует auth (selelр может ещё не быть залогинен).
Файл: src/app/api/wms/clients/invite/[token]/route.ts
DELETE /api/wms/clients/invite/[token] Revoke — только для invited_by из этого ФФ.
DELETE /api/wms/clients/invite/[token]

Revoke — только для invited_by из этого ФФ.
Файл: src/app/api/wms/clients/invite/[token]/route.ts
POST /api/wms/clients/invite/[token]/accept 3 сценария:
POST /api/wms/clients/invite/[token]/accept

3 сценария:
 A. **Authenticated existing seller** → link platform_user к wms_client.
    body: {} (просто accept current user)
 B. **Existing user, не залогинен** → требуется email+password в body для верификации.
    body: { email, password }
 C. **Новый user (no platform_user with this email)** → создать аккаунт.
    body: { email, password, phone, full_name, agreement_accepted }

Результат:
 - wms_client.platform_user_id = current/created user.id
 - Если client_id=NULL в invite → создать wms_client из invite.name/inn
 - Создать wms_user в этом ФФ (role='client') если нет
 - Set session ff_id = invite.ff_id (selelр сразу попадает в кабинет этого ФФ)
Файл: src/app/api/wms/clients/invite/[token]/accept/route.ts
POST /api/wms/clients/invite/new body: { email, name, inn?, expires_in_days? (default 14) }
POST /api/wms/clients/invite/new
body: { email, name, inn?, expires_in_days? (default 14) }

Создаёт invite для НОВОГО клиента (без существующей wms_client записи).
При accept селлером — создастся wms_client с этими данными + linked
platform_user.

Доступ: только staff ФФ (не клиенты-селлеры).
Файл: src/app/api/wms/clients/invite/new/route.ts
GET /api/wms/clients/invites Возвращает список invite'ов в текущем ФФ.
GET /api/wms/clients/invites?status=pending|accepted|revoked|expired|all

Возвращает список invite'ов в текущем ФФ.
Для UI каталога приглашений (/wms/clients/invites).
Файл: src/app/api/wms/clients/invites/route.ts
GET /api/wms/courier Параметр ?status=pending|in_delivery — фильтр.
GET /api/wms/courier — список DBS-заявок назначенных текущему курьеру.
Параметр ?status=pending|in_delivery — фильтр.

POST body: { request_id, action: 'pickup'|'deliver'|'refuse', notes? }
  pickup → status='in_progress', courier_picked_at = NOW (через notes)
  deliver → status='completed', delivered_at = NOW
  refuse → status='canceled', notes
Файл: src/app/api/wms/courier/route.ts
POST /api/wms/courier (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/courier/route.ts
GET /api/wms/cycle-count Возвращает список товаров для сегодняшней (или указанной даты) cycle count.
GET /api/wms/cycle-count?date=YYYY-MM-DD

Возвращает список товаров для сегодняшней (или указанной даты) cycle count.
По умолчанию today, не показывает completed.

Response: { items: [{ id, product_id, product_sku, product_name, abc_class, expected_qty, ... }] }
Файл: src/app/api/wms/cycle-count/route.ts
POST /api/wms/cycle-count body: { date?: YYYY-MM-DD }
POST /api/wms/cycle-count/generate
body: { date?: YYYY-MM-DD }

Genenerate cycle count plan для указанной даты (default today) на основе
ABC-classification. Idempotent — повторный call для same date не дублирует.

ABC классификация по 30-day movement count:
  - A (top 20%): scheduled каждые 30 дней
  - B (next 30%): каждые 90 дней
  - C (остальные 50%): каждые 180 дней

Логика выбора SKU на день: те у которых last_counted_at старше cycle_days.
Файл: src/app/api/wms/cycle-count/route.ts
POST /api/wms/cycle-count/count Завершает один cycle-count item: counter записывает actual qty.
POST /api/wms/cycle-count/count

Завершает один cycle-count item: counter записывает actual qty.
Discrepancy вычисляется автоматически (GENERATED COLUMN).
Audit log для compliance.

Не двигает stock — это compare-only operation (full inventory с adjust-movements
— отдельный flow через inventory/[id]/finalize).
Файл: src/app/api/wms/cycle-count/count/route.ts
GET /api/wms/departments (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/departments/route.ts
POST /api/wms/departments (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/departments/route.ts
PATCH /api/wms/departments/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/departments/[id]/route.ts
DELETE /api/wms/departments/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/departments/[id]/route.ts
POST /api/wms/device/register (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/device/register/route.ts
GET /api/wms/devices Admin-only listing of TSD-устройств в реестре tenant'а. Возвращает sorted
GET /api/wms/devices

Admin-only listing of TSD-устройств в реестре tenant'а. Возвращает sorted
by last_seen_at DESC чтобы недавно активные были сверху.

Используется WMS dashboard'ом — admin видит «у нас 5 ТСД, последний активен
3 мин назад, у Иванова MovFast S55, у Петрова Zebra TC52».
Файл: src/app/api/wms/devices/route.ts
GET /api/wms/events (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/events/route.ts
GET /api/wms/fbo/export/returns (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/export/returns/route.ts
GET /api/wms/fbo/export/storage-events (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/export/storage-events/route.ts
GET /api/wms/fbo/export/supplies (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/export/supplies/route.ts
GET /api/wms/fbo/metrics (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/metrics/route.ts
POST /api/wms/fbo/returns/from-supply/[supplyId]/initiate Manager-facing: "Я получил N единиц назад с МП по поставке FBO-2026-0001".
POST /api/wms/fbo/returns/from-supply/[supplyId]/initiate

Manager-facing: "Я получил N единиц назад с МП по поставке FBO-2026-0001".

Creates wms_return rows linked to the source FBO supply.
One wms_return row per product_id (mirrors WB sync-returns schema).

Body:
  {
    items: [{ product_id, qty, reason? }],
    notes?: string,
    marketplace?: 'wb'|'ozon'|'ym'|'kaspi'   // default 'wb'
  }

Idempotency:
  Same product_id for same supply within last 60 seconds → 409 (duplicate).
  External_id = "fbo-return-{supplyId}-{productId}" — deterministic, tenant-scoped.

Multi-tenant: ff_id from session, supply ownership verified.
RBAC: manager+ (stock-affecting operation).
Файл: src/app/api/wms/fbo/returns/from-supply/[supplyId]/initiate/route.ts
GET /api/wms/fbo/supplies (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/route.ts
POST /api/wms/fbo/supplies (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/route.ts
POST /api/wms/fbo/supplies/[id]/bind-wb (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/bind-wb/route.ts
GET /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/box-cargo-bind/route.ts
POST /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/box-cargo-bind/route.ts
DELETE /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/box-cargo-bind/route.ts
POST /api/wms/fbo/supplies/[id]/complete Marks an FBO supply as completed and triggers storage billing adjustments.
POST /api/wms/fbo/supplies/[id]/complete

Marks an FBO supply as completed and triggers storage billing adjustments.

What happens on completion:
  1. Validates: type='fbo', status in ['confirmed','in_progress'] (not already completed)
  2. Transitions wms_request.status → 'completed', sets completed_at
  3. For each wms_request_item:
     a. Inserts a wms_movement of type='ship' with qty = -actual_qty
        (the movement trigger from migration 096 auto-decrements qty_available)
     b. Increments wms_stock.qty_mp += actual_qty (goods moved to MP warehouse)
     c. Inserts a wms_storage_event with event_type='fbo_shipped', qty_delta=-actual_qty
  4. Writes wms_audit_log entry

Idempotency: if request is already 'completed', returns 200 with idempotent=true.
Re-running does NOT double-decrement stock (guard on status check before tx).

Multi-tenant: all SQL operations filter by ff_id from session (never from client).

RBAC: manager+ (completing supply = irreversible stock operation).
Файл: src/app/api/wms/fbo/supplies/[id]/complete/route.ts
GET /api/wms/fbo/supplies/[id]/manifest Транспортный манифест для FBO-поставки на склад МП.
GET /api/wms/fbo/supplies/[id]/manifest

Транспортный манифест для FBO-поставки на склад МП.

Содержит:
  - Шапка: ФФ (отправитель) + МП-склад (получатель) + supply_type + даты
  - mp_pass_code (пропуск на склад МП)
  - Список коробок с SSCC
  - Содержимое (по SKU суммировано)
  - Totals: общее кол-во SKU, units, boxes, pallets

Можно distill в PDF через render-pdf endpoint (`/api/wms/pdf?source=fbo&id=...`).

Возвращает JSON для UI; для печати — PDF через отдельный path /wms/print/fbo/[id].
Файл: src/app/api/wms/fbo/supplies/[id]/manifest/route.ts
POST /api/wms/fbo/supplies/[id]/propose-date (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/propose-date/route.ts
POST /api/wms/fbo/supplies/[id]/qr-upload (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/qr-upload/route.ts
POST /api/wms/fbo/supplies/[id]/respond-proposal (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/respond-proposal/route.ts
GET /api/wms/fbo/supplies/[id]/sscc (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/[id]/sscc/route.ts
POST /api/wms/fbo/supplies/[id]/sscc body: { box_count: number, box_kind?: 'mono_box'|'mix_box'|'pallet' }
POST /api/wms/fbo/supplies/[id]/sscc
body: { box_count: number, box_kind?: 'mono_box'|'mix_box'|'pallet' }

Создаёт N коробок для FBO supply + генерирует SSCC для каждой.
SSCC формируется по GS1 mod-10 алгоритму (см. lib/wms/sscc.ts).

Возвращает массив созданных коробок с их SSCC для printing.

GET /api/wms/fbo/supplies/[id]/sscc — список существующих коробок supply.
Файл: src/app/api/wms/fbo/supplies/[id]/sscc/route.ts
GET /api/wms/fbo/supplies/[id]/submit-to-mp Запрашивает статус supply в MP (если уже submitted).
GET /api/wms/fbo/supplies/[id]/submit-to-mp

Запрашивает статус supply в MP (если уже submitted).
Обновляет mp_accepted_qty/rejected_qty если приёмка завершена.
Файл: src/app/api/wms/fbo/supplies/[id]/submit-to-mp/route.ts
POST /api/wms/fbo/supplies/[id]/submit-to-mp Создаёт FBO-поставку в кабинете маркетплейса (WB/Ozon/YM) через API
POST /api/wms/fbo/supplies/[id]/submit-to-mp

Создаёт FBO-поставку в кабинете маркетплейса (WB/Ozon/YM) через API
и сохраняет внешний supply ID в mp_pass_code.

Wave 25 (2026-05-17): добавлены Ozon + YM submit (raньше был только WB).
  - WB: name = supply.number
  - Ozon: warehouse_id (требует marketplace_warehouse), timeslot, items[], supply_type
  - YM: warehouse_id, planned_date (planned_date), items[]

Препорассло:
  - Supply должен быть type='fbo', status='draft' or 'confirmed'
  - marketplace должен быть задан
  - У клиента валидный credential для этого MP
  - mp_pass_code ещё не заполнен (защита от двойного submit)

После успеха — сохраняем external supply ID в mp_pass_code + transition в 'confirmed'.
Файл: src/app/api/wms/fbo/supplies/[id]/submit-to-mp/route.ts
GET /api/wms/fbo/supplies/[id]/timeline Возвращает chronological timeline событий FBO заявки:
GET /api/wms/fbo/supplies/[id]/timeline

Возвращает chronological timeline событий FBO заявки:
  - status transitions (draft → confirmed → in_progress → completed)
  - propose-date, accept/reject proposal
  - bind-wb, qr-upload, box-cargo binds
  - submit-to-mp (Ozon/YM)

Источник — wms_audit_log (B4 hybrid audit, см. 054-wms-audit-log.sql).
Колонки в схеме: actor_id (FK на wms_user), actor_email snapshot,
action, entity_table, entity_id, summary, before_data, after_data,
created_at.
Файл: src/app/api/wms/fbo/supplies/[id]/timeline/route.ts
POST /api/wms/fbo/supplies/seller-create (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/fbo/supplies/seller-create/route.ts
GET /api/wms/fbs/pack-photo Список фото по заявке.
GET /api/wms/fbs/pack-photo?request_id=...
Список фото по заявке.
Файл: src/app/api/wms/fbs/pack-photo/route.ts
POST /api/wms/fbs/pack-photo multipart/form-data: file=<image>, request_id, box_id?, kind?='pack', notes?
POST /api/wms/fbs/pack-photo
multipart/form-data: file=<image>, request_id, box_id?, kind?='pack', notes?

Фотофиксация коробов/состояния FBS-сборки. Защита от disputes с
маркетплейсом (если WB заявит «прислали не то / повреждено» — мы
показываем фото перед отгрузкой).

Файлы в public/uploads/wms/fbs/{request_id}/{kind}_{uuid}.{ext}
Регистрация в wms_fbs_pack_photo.
Файл: src/app/api/wms/fbs/pack-photo/route.ts
POST /api/wms/fbs/scan body: { code, supply_id?, qty? }
POST /api/wms/fbs/scan
body: { code, supply_id?, qty? }

Scan flow для кладовщика. Принимает code (штрихкод товара или
external_task_id с маркетплейса). Если supply_id задан — ищет в этой
supply, иначе — в любой активной FBS-supply.

Логика:
  1. Найти task (request_item) по external_task_id ИЛИ по barcode product
  2. Инкрементировать scanned_qty
  3. Если scanned_qty >= planned_qty → task_status='packed'
  4. Вернуть task с актуальными данными для UI

Идемпотентность (invariant #3): можно передать `Idempotency-Key` header
(UUID от ТСД). Повтор с тем же ключом + same body → cached 2xx ответ без
побочных эффектов. Повтор с другим body → 422 idempotency-key-mismatch.
Без header работает как раньше (backward compat).
Файл: src/app/api/wms/fbs/scan/route.ts
GET /api/wms/fbs/supplies Возвращает supplies (= wms_request type IN (fbs/fbo/dbs)) с aggregated
GET /api/wms/fbs/supplies?status=&marketplace=&client_id=&search=&period=

Возвращает supplies (= wms_request type IN (fbs/fbo/dbs)) с aggregated
task-progress: сколько tasks всего, сколько packed, units planned/scanned.

Используется в /wms/fbs (Variant D — глобальный список с expand).
Файл: src/app/api/wms/fbs/supplies/route.ts
GET /api/wms/fbs/supplies/export Выгружает поставки FBS/FBO/DBS в xlsx.
GET /api/wms/fbs/supplies/export?status=&client_id=&shipment_type=&types=&search=

Выгружает поставки FBS/FBO/DBS в xlsx.
SkladBot pattern: кнопка «Экспорт в Excel» в архиве.
Файл: src/app/api/wms/fbs/supplies/export/route.ts
GET /api/wms/fbs/tasks/[id] id = supply (= wms_request) id
GET /api/wms/fbs/tasks/[id]
id = supply (= wms_request) id

Возвращает список tasks (request_items) с product/cell/sticker информацией.
Используется в expand-row на /wms/fbs и в /wms/fbs/work.
Файл: src/app/api/wms/fbs/tasks/[id]/route.ts
PATCH /api/wms/fbs/tasks/[id] id = task (= request_item) id
PATCH /api/wms/fbs/tasks/[id]
id = task (= request_item) id
body: { task_status?, scanned_qty?, external_task_id?, sticker_label?, sticker_url?, pick_cell_id? }
Файл: src/app/api/wms/fbs/tasks/[id]/route.ts
GET /api/wms/finance/reconciliation Акт сверки между ФФ и клиентом за период.
GET /api/wms/finance/reconciliation?client_id=&from=YYYY-MM-DD&to=YYYY-MM-DD

Акт сверки между ФФ и клиентом за период.

Возвращает:
  - opening_balance: сумма к оплате до начала периода (issued+paid за всё время до from)
  - period.charges: сумма выставленных счетов в период
  - period.payments: сумма оплат в период (по wms_invoice.paid_at)
  - closing_balance: сальдо на конец (opening + charges - payments)
  - rows: детальный список операций
Файл: src/app/api/wms/finance/reconciliation/route.ts
GET /api/wms/finance/storage-forecast Прогноз заполнения склада + расходов на хранение на следующие N дней
GET /api/wms/finance/storage-forecast?client_id=&days=30

Прогноз заполнения склада + расходов на хранение на следующие N дней
на основе текущего volume + история движений за последние 30 дней.

Алгоритм:
 1. Текущий объём = SUM(stock.qty × volume_m3)
 2. Среднее изменение в день за прошлые 30 дн (по wms_movement)
 3. Прогноз = current + avg_daily_change × days
 4. Cost forecast = volume × tariff × days
Файл: src/app/api/wms/finance/storage-forecast/route.ts
GET /api/wms/finance/storage-log Журнал хранения. Каждая строка = ежедневный snapshot для клиента.
GET /api/wms/finance/storage-log?client_id=&from=YYYY-MM-DD&to=YYYY-MM-DD

Журнал хранения. Каждая строка = ежедневный snapshot для клиента.
Селлер видит только свой client_id (auto-filter).

Возвращает:
 - items: список логов
 - total_charged: сумма за выбранный период
 - clients: уникальные клиенты в выборке
Файл: src/app/api/wms/finance/storage-log/route.ts
POST /api/wms/finance/storage-log/run Ручной триггер дневного начисления хранения. Используется UI-кнопкой
POST /api/wms/finance/storage-log/run

Ручной триггер дневного начисления хранения. Используется UI-кнопкой
«Начислить сейчас» в /wms/finance/storage-log. Логика идентична cron,
но scope только текущий ФФ (через getTenantId) — без secret'а.

Идемпотентно: повторный запуск в тот же день не дублирует записи.
Файл: src/app/api/wms/finance/storage-log/run/route.ts
POST /api/wms/finance/tariffs/import Body: text/csv plain или {csv: "..."} или multipart с file
POST /api/wms/finance/tariffs/import

Body: text/csv plain или {csv: "..."} или multipart с file

Формат CSV (первая строка — header):
  client_name,service_name,price,effective_from,effective_to
  "ИП Гусейнов","Хранение обработанного 1 куб",60,2026-05-01,
  "ИП Губаев","Приёмка короба",100,2026-05-01,

Логика:
 - Резолвит client_id по client_name (ILIKE с %)
 - Резолвит service_id по service_name (ILIKE с %)
 - Если оба найдены → INSERT в wms_client_tariff
 - Идемпотентно: ON CONFLICT (client_id, service_id, effective_from) DO UPDATE

Ответ: { processed, inserted, updated, errors: [...] }
Файл: src/app/api/wms/finance/tariffs/import/route.ts
POST /api/wms/inbound/receive body: {
POST /api/wms/inbound/receive
body: {
  request_id, code, qty,
  cell_address? | cell_id?,
  lot_number? mfg_date? expiry_date?   // для товаров с lot_tracking=true
}

Принять единицу товара на склад в рамках inbound-заявки.
 1. Резолвим product по штрихкоду или sku (в скоупе клиента заявки)
 2. Если товар требует lot_tracking — lot_number обязателен
 3. Найти/создать wms_request_item, инкрементировать actual_qty
 4. Если задана ячейка — записать movement type='receive' to_cell_id
    + bump qty_available в wms_stock + upsert wms_stock_lot если lot_number есть
 5. Если ячейка не задана — товар принят но не размещён (можно разместить позже)

Идемпотентность (invariant #3): можно передать `Idempotency-Key` header
(UUID от ТСД). Повтор с тем же ключом + same body → cached 2xx ответ без
побочных эффектов. Повтор с другим body → 422 idempotency-key-mismatch.
Без header работает как раньше (backward compat).

Возвращает: { ok, item: { sku, name, planned_qty, actual_qty, percent }, suggested_cell?: {...} }
Файл: src/app/api/wms/inbound/receive/route.ts
POST /api/wms/inbound/undo body: { request_id, code, qty, cell_address? | cell_id? }
POST /api/wms/inbound/undo
body: { request_id, code, qty, cell_address? | cell_id? }

**Compensating action** для отмены `/api/wms/inbound/receive`.

Симметричный к receive endpoint: decrement-ит counters обратно.
Используется ТСД-приложением (`ReceiveScreen.onUndo()`) когда оператор
нажимает «Отменить» после ошибочного scan'а.

До этого endpoint'а undo был **optimistic-only** на устройстве — UI
откатывался, но БД оставалась с +qty → рассинхрон между ТСД и
web-кабинетом. См. CHANGELOG wms-tsd 0.14.4 Found (TODO) bug #2.

Логика:
 1. Резолвим product по коду (точно как receive)
 2. Decrement `wms_request_item.actual_qty` (clamp 0)
 3. Insert `wms_movement` type='adjust' qty=-N (audit-trail)
 4. Decrement `wms_stock.qty_available` (clamp 0)
 5. Audit-event `inbound_receive_undone` в `wms_request_event`

Idempotency: `Idempotency-Key` header support как у receive
(повтор с тем же ключом → cached response).

Returns: { ok, item: { sku, name, planned_qty, actual_qty, percent } }
Файл: src/app/api/wms/inbound/undo/route.ts
POST /api/wms/integrations/kaspi/sync Подтягивает active orders из Kaspi (NEW + ACCEPTED + ASSEMBLE) и создаёт
POST /api/wms/integrations/kaspi/sync?client_id=

Подтягивает active orders из Kaspi (NEW + ACCEPTED + ASSEMBLE) и создаёт
wms_request type='fbs' с marketplace='kaspi'. Match по SKU (offer code).

Idempotency: если wms_request с (ff_id, client_id, type='fbs', marketplace='kaspi',
external_id) уже существует — skip.

Если client_id не указан — sync для всех Kaspi-подключённых клиентов.
Файл: src/app/api/wms/integrations/kaspi/sync/route.ts
POST /api/wms/integrations/kaspi/sync-stocks Подтягивает текущие available stocks из wms_stock и отправляет в Kaspi.
POST /api/wms/integrations/kaspi/sync-stocks?client_id=

Подтягивает текущие available stocks из wms_stock и отправляет в Kaspi.
Match по product.sku (наша система) → Kaspi offer code.

Если client_id не указан — обновление для всех Kaspi-подключённых клиентов.
Файл: src/app/api/wms/integrations/kaspi/sync-stocks/route.ts
POST /api/wms/integrations/ozon/sync 1. Cards (product list + info_list) → match по offer_id (sku) или barcode
POST /api/wms/integrations/ozon/sync?client_id=

1. Cards (product list + info_list) → match по offer_id (sku) или barcode
   → ставим external_marketplace_id = product_id, image_url ← primary_image
2. Orders FBS (awaiting_packaging) → wms_request type='fbs'
3. Stocks (FBS) → external_marketplace_qty
Файл: src/app/api/wms/integrations/ozon/sync/route.ts
POST /api/wms/integrations/ozon/sync-all Прогоняет OZON sync для всех клиентов с активным OZON-ключом sequentially
POST /api/wms/integrations/ozon/sync-all

Прогоняет OZON sync для всех клиентов с активным OZON-ключом sequentially
(delay 500ms между клиентами), возвращает summary.
Файл: src/app/api/wms/integrations/ozon/sync-all/route.ts
POST /api/wms/integrations/ozon/sync-stocks body: { client_id?: string }
POST /api/wms/integrations/ozon/sync-stocks
body: { client_id?: string }

Pull остатков OZON через `ozonPullStocks` и обновление `wms_product_external`
для существующих привязок (resolve product по offer_id или product_id из
external_sku/external_id).

Если у нас в БД нет привязки для этого OZON-product — НЕ создаём
автоматически (требуется явный binding admin'ом, чтобы не наплодить
мусорных связок). В summary показываем сколько unbound.
Файл: src/app/api/wms/integrations/ozon/sync-stocks/route.ts
GET /api/wms/integrations/status Aggregated integrations health для tenant'а.
GET /api/wms/integrations/status

Aggregated integrations health для tenant'а.

Возвращает per-marketplace:
  - clients_total — сколько клиентов имеют credential для этого MP
  - clients_valid — сколько с is_valid=TRUE
  - clients_invalid — сколько с is_valid=FALSE (нужен fix)
  - last_check_at — самая свежая проверка
  - errors — последние 5 уникальных last_error из invalid creds

А также cron статусы для интеграционных задач (wb-sync-all, ozon-stocks-sync, ym-stocks-sync).
Файл: src/app/api/wms/integrations/status/route.ts
POST /api/wms/integrations/wb/backfill-warehouses Бэкфилл seller_warehouse_id для legacy supplies где он NULL.
POST /api/wms/integrations/wb/backfill-warehouses?client_id=&limit=100

Бэкфилл seller_warehouse_id для legacy supplies где он NULL.
Тяжёлая операция: по одному WB API call на каждую supply (rate limit 100/min).

Логика:
 - Берёт supplies с marketplace='wb' AND seller_warehouse_id IS NULL
 - Для каждого supply дёргает /api/v3/supplies/{id}/orders
 - Берёт warehouseId из первого order (supply обычно одного склада)
 - Резолвит wms_seller_warehouse → seller_warehouse_id
 - UPDATE wms_request

Вызывается вручную из admin UI или batch'ом.
Файл: src/app/api/wms/integrations/wb/backfill-warehouses/route.ts
POST /api/wms/integrations/wb/create-supply Body: { client_id, request_ids: [], name?: 'Поставка от ...' }
POST /api/wms/integrations/wb/create-supply
Body: { client_id, request_ids: [], name?: 'Поставка от ...' }

1. Берёт WB credentials клиента
2. Создаёт supply в WB → получает WB-GI-xxx
3. Для каждого request_id берёт external_id и добавляет в supply
4. Записывает supply_id в delivery_number всех request'ов
5. Меняет статус request'ов на 'in_progress' если они были draft

Возвращает { supply_id, added: N, errors: [] }
Файл: src/app/api/wms/integrations/wb/create-supply/route.ts
POST /api/wms/integrations/wb/deliver-supply Body: { client_id, supply_id: 'WB-GI-xxx' }
POST /api/wms/integrations/wb/deliver-supply
Body: { client_id, supply_id: 'WB-GI-xxx' }

Передаёт WB Supply в доставку (закрывает для редактирования).
После этого WB будет ждать сканирования QR-кода поставки на пункте приёма.
Все наши request'ы с этим delivery_number переходят в 'completed'.
Файл: src/app/api/wms/integrations/wb/deliver-supply/route.ts
POST /api/wms/integrations/wb/stickers body: { client_id, order_ids: string[] } // WB sticker IDs (10-digit)
POST /api/wms/integrations/wb/stickers
body: { client_id, order_ids: string[] }   // WB sticker IDs (10-digit)

WB API endpoint /api/v3/orders/stickers возвращает stickers как base64 ZPL/PNG.
Мы извлекаем PNG и стрелили в ZIP / возвращаем как base64 array.
Файл: src/app/api/wms/integrations/wb/stickers/route.ts
GET /api/wms/integrations/wb/supply-barcode Возвращает base64 QR-код (или SVG) поставки, для печати на стикере.
GET /api/wms/integrations/wb/supply-barcode?client_id=&supply_id=&type=png

Возвращает base64 QR-код (или SVG) поставки, для печати на стикере.
Файл: src/app/api/wms/integrations/wb/supply-barcode/route.ts
POST /api/wms/integrations/wb/sync (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/integrations/wb/sync/route.ts
POST /api/wms/integrations/wb/sync-all Прогоняет /api/wms/integrations/wb/sync для каждого клиента с активным
POST /api/wms/integrations/wb/sync-all?history=N

Прогоняет /api/wms/integrations/wb/sync для каждого клиента с активным
WB-ключом sequentially (чтобы не перегрузить WB API rate-limit).

Используется как «cron one-shot»: разово прогнать всех 23 клиентов с
валидными ключами + получить summary report.
Файл: src/app/api/wms/integrations/wb/sync-all/route.ts
POST /api/wms/integrations/wb/sync-returns body: { client_id?: string, days?: number = 30 }
POST /api/wms/integrations/wb/sync-returns
body: { client_id?: string, days?: number = 30 }

Подтягивает FBS возвраты от покупателей через WB API и кладёт в wms_return.
Идемпотентно (UNIQUE на (ff_id, marketplace, external_id) → ON CONFLICT UPDATE
external_status, без перезаписи статусов нашей стороны).

Создаёт запись со status='pending' для приёмщика. UI приёмщика на /wms/returns.
Файл: src/app/api/wms/integrations/wb/sync-returns/route.ts
POST /api/wms/integrations/wb/sync-statuses body: { client_id?: string, days?: number = 30 }
POST /api/wms/integrations/wb/sync-statuses
body: { client_id?: string, days?: number = 30 }

Подтягивает АКТУАЛЬНЫЕ статусы для existing WB-заказов / поставок:
  1. Тянет /api/v3/orders за последние N дней (pullOrdersHistory)
  2. Тянет /api/v3/supplies (pullSupplies)
  3. Для каждого существующего нашего order'а с external_id:
     - Если он есть в orders history с supplyId → обновляем delivery_number
     - Если его НЕТ в orders history (старше 90 дней или удалён) → mark stale
  4. Для каждой нашей supply с external_id LIKE 'WB-GI-%':
     - WB supply.done=true → наш status='completed'
     - WB supply.done=false → наш status='in_progress' (если был draft/confirmed)
     - Supply отсутствует у WB вообще → mark missing (warning, не cancel)

Этот endpoint complementary к существующему /sync который обрабатывает только
новые orders. Идемпотентный — можно вызывать сколько угодно.

Если client_id не передан — обновляет все клиентов FF с валидным WB-ключом.
Файл: src/app/api/wms/integrations/wb/sync-statuses/route.ts
POST /api/wms/integrations/ym/sync Cards + orders + stocks (Yandex Market). Stocks резолвятся через
POST /api/wms/integrations/ym/sync?client_id=

Cards + orders + stocks (Yandex Market). Stocks резолвятся через
`wms_product_external` (marketplace='ym', external_sku=offer_id) — не
создаются автоматически если binding отсутствует, попадают в `missing`.
Файл: src/app/api/wms/integrations/ym/sync/route.ts
POST /api/wms/integrations/ym/sync-stocks body: { client_id?: string }
POST /api/wms/integrations/ym/sync-stocks
body: { client_id?: string }

Pull остатков Yandex Market через `ymPullStocks` и обновление
`wms_product_external` для существующих привязок (resolve по marketplace='ym'
+ external_sku=offer_id ИЛИ external_id=offer_id).

Если у нас в БД нет привязки для offer_id из YM — НЕ создаём
автоматически. В summary показываем сколько unbound.

NB: YM возвращает (offer × warehouse) — несколько строк на один offer если
товар лежит на разных складах. Мы суммируем общий fit per offer_id.
Файл: src/app/api/wms/integrations/ym/sync-stocks/route.ts
POST /api/wms/inventory/[id]/analyze-variance Топ-причины расхождений */
Топ-причины расхождений */
  primary_causes: Array<{
    cause: string;
    severity: 'high' | 'medium' | 'low';
    affected_skus: string[];
    explanation: string;
  }>;
  /** Рекомендации для предотвращения */
  recommendations: string[];
  /** Patterns обнаруженные в данных */
  patterns: string[];
  /** Critical SKUs которые требуют немедленного внимания */
  critical_skus: Array<{
    sku: string;
    name: string;
    diff: number;
    severity: 'high' | 'medium' | 'low';
    likely_cause: string;
  }>;
  /** Overall assessment */
  assessment: string;
}

/**
POST /api/wms/inventory/[id]/analyze-variance

AI-анализ расхождений после finalize inventory.

Логика:
  1. Берём snapshot variances из wms_inventory_finalize_log
  2. Дополняем контекстом — для каждого SKU с расхождением:
     - История movements за 30 дней
     - Список заявок где этот SKU участвовал
  3. Отправляем в gpt-5.5 → анализ причин (систематические ошибки приёмки
     vs кражи vs ошибки сканирования vs ошибки списания)
  4. Returns: primary_causes, recommendations, patterns, critical_skus

Use-case: оператор-менеджер склада после большой инвентаризации получает
AI-инсайты что именно пошло не так и как исправить процессы.
Файл: src/app/api/wms/inventory/[id]/analyze-variance/route.ts
POST /api/wms/inventory/[id]/finalize body: { dry_run?: boolean, notes?: string }
POST /api/wms/inventory/[id]/finalize
body: { dry_run?: boolean, notes?: string }

Финализация инвентаризации:
  1. Берём все items заявки с (actual_qty != planned_qty)
  2. Для каждого создаём wms_movement type='adjust' с qty = diff
     (положительный = нашли больше, отрицательный = недостача)
  3. Применяем delta к wms_stock.qty_available
  4. Меняем request.status='completed'
  5. Снимок в wms_inventory_finalize_log

dry_run=true → возвращает только variances без записи (preview).

Improvement над SkladBot: dry_run preview перед commit, чтобы оператор
увидел общую сумму расхождений до подтверждения.
Файл: src/app/api/wms/inventory/[id]/finalize/route.ts
POST /api/wms/inventory/scan body: { request_id, code, qty=1 }
POST /api/wms/inventory/scan
body: { request_id, code, qty=1 }

Сканер для инвентаризации. Логика:
 1. Resolve product по barcode/sku в скоупе клиента заявки
 2. Find/create wms_request_item с planned_qty = stock.qty_available (snapshot)
 3. Инкремент actual_qty + scanned_qty
 4. Не создаёт movement — это сделает finalize-endpoint после завершения
    инвентаризации (diff между actual и stock → adjust-movement)

Возвращает: { ok, item: { sku, name, planned, actual, diff, percent } }
Файл: src/app/api/wms/inventory/scan/route.ts
GET /api/wms/invoices (нет описания)
GET /api/wms/invoices?client_id=&status=&source_type=
Файл: src/app/api/wms/invoices/route.ts
POST /api/wms/invoices body: {
POST /api/wms/invoices
body: {
  client_id, source_type ('manual'|'request'|'storage_period'),
  source_request_id?, source_period?,
  lines: [{service_id?, name, unit_price, qty, discount_pct?}],
  discount_pct?, notes?
}
Файл: src/app/api/wms/invoices/route.ts
GET /api/wms/invoices/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/invoices/[id]/route.ts
PATCH /api/wms/invoices/[id] (нет описания)
PATCH — изменить статус (issue / mark paid / cancel) или discount.
Файл: src/app/api/wms/invoices/[id]/route.ts
DELETE /api/wms/invoices/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/invoices/[id]/route.ts
GET /api/wms/invoices/[id]/act-data (нет описания)
GET /api/wms/invoices/[id]/act-data — данные для печати акта
Файл: src/app/api/wms/invoices/[id]/act-data/route.ts
GET /api/wms/invoices/[id]/export-1c Экспорт invoice в формате 1С (упрощённый УПД XML).
GET /api/wms/invoices/[id]/export-1c?format=upd_xml

Экспорт invoice в формате 1С (упрощённый УПД XML).
Регистрируется в wms_invoice_export_log для audit.
Файл: src/app/api/wms/invoices/[id]/export-1c/route.ts
POST /api/wms/invoices/[id]/lines body: { service_id?, service_name, unit_price, qty, discount_pct?, vat_rate?, notes? }
POST /api/wms/invoices/[id]/lines
body: { service_id?, service_name, unit_price, qty, discount_pct?, vat_rate?, notes? }

Добавляет строку услуги в draft-инвойс. Авто-пересчёт total в transaction.
Запрещено для не-draft статусов (issued/paid/canceled).

VAT logic:
  - vat_rate: explicit (body) ИЛИ defaultVatRate(ff.tax_system) (20 для ОСН, -1 для УСН/патент)
  - vat_amount + total_without_vat — auto через calcVat() в зависимости от invoice.vat_included
  - Trigger wms_invoice_recalc_vat пересчитывает invoice.total/total_vat/total_without_vat по сумме lines
Файл: src/app/api/wms/invoices/[id]/lines/route.ts
DELETE /api/wms/invoices/[id]/lines/[line_id] Удаляет строку из draft-инвойса. Пересчитывает total.
DELETE /api/wms/invoices/[id]/lines/[line_id]
Удаляет строку из draft-инвойса. Пересчитывает total.
Файл: src/app/api/wms/invoices/[id]/lines/[line_id]/route.ts
POST /api/wms/invoices/from-request body: { request_id, services?: [{service_id, qty, unit_price?}] }
POST /api/wms/invoices/from-request
body: { request_id, services?: [{service_id, qty, unit_price?}] }

Создаёт invoice по заявке. Если services не передан — пытается посчитать
автоматически по типу заявки (приёмка → услуга «Приёмка», шт. = total qty).

Идемпотентно: если уже есть инвойс с source_request_id = request_id —
возвращает существующий с warning.
Файл: src/app/api/wms/invoices/from-request/route.ts
GET /api/wms/kiz (нет описания)
GET /api/wms/kiz?product_id=&client_id=&state=&search=
Файл: src/app/api/wms/kiz/route.ts
POST /api/wms/kiz body: { codes: string[] | string, product_id?, client_id? }
POST /api/wms/kiz
body: { codes: string[] | string, product_id?, client_id? }

Bulk upload: парсит каждый код, создаёт wms_kiz запись.
Дубли (uniq raw_code per ff) → пропускаем, считаем skipped.
Файл: src/app/api/wms/kiz/route.ts
GET /api/wms/kiz/[id] (нет описания)
GET /api/wms/kiz/[id] — детали + история событий
Файл: src/app/api/wms/kiz/[id]/route.ts
PATCH /api/wms/kiz/[id] body: { state?, product_id?, box_id?, request_id?, notes? }
PATCH /api/wms/kiz/[id]
body: { state?, product_id?, box_id?, request_id?, notes? }

Изменение state записывает event. Проверяет валидность переходов.
Файл: src/app/api/wms/kiz/[id]/route.ts
POST /api/wms/kiz/bulk-print Bulk-печать N этикеток КИЗ. 2 режима:
POST /api/wms/kiz/bulk-print

Bulk-печать N этикеток КИЗ. 2 режима:
  1. `kiz_ids: string[]` — точечный выбор конкретных КИЗ
  2. `product_id + qty + state?` — взять N доступных КИЗ для товара
     (default state='in_stock', можно 'uploaded')

Body:
  - kiz_ids?: string[] | product_id + qty
  - format: 'pdf' | 'zpl' | 'tspl' | 'epl'
  - copies_per_label?: 1
  - template_id?: optional override (default — kind=kiz is_default)

Логирует печать в wms_label_print_log с bulk=true.
Файл: src/app/api/wms/kiz/bulk-print/route.ts
GET /api/wms/kiz/export Выгружает КИЗ в Excel (xlsx). Для отчётов / возврата клиенту.
GET /api/wms/kiz/export?client_id=&product_id=&state=
Выгружает КИЗ в Excel (xlsx). Для отчётов / возврата клиенту.

2026-05-16: migrated с xlsx@0.18.5 (2 HIGH CVE без upstream fix) на exceljs@4.4
Файл: src/app/api/wms/kiz/export/route.ts
POST /api/wms/kiz/parse body: { code: string }
POST /api/wms/kiz/parse
body: { code: string }

Server-side парсинг (для scan-input). Возвращает извлечённые поля.
Файл: src/app/api/wms/kiz/parse/route.ts
POST /api/wms/kiz/scan-reprint Single-scan reprint flow: оператор сканирует DataMatrix с порванной/
POST /api/wms/kiz/scan-reprint

Single-scan reprint flow: оператор сканирует DataMatrix с порванной/
утерянной этикетки → endpoint находит КИЗ в DB → рендерит новую
этикетку → возвращает файл.

Body:
  - raw_code: string  (full DataMatrix как пришёл со сканера)
  - OR: serial: string  (последние 13 chars если по ним проще искать)
  - format: 'pdf' | 'zpl' | 'tspl' | 'epl'
  - copies?: 1  (default)
  - template_id?: optional override

Returns:
  - 200 file blob (Content-Type per format)
  - 404 если КИЗ не найден
  - 400 если raw_code невалидный

Side-effects:
  - printed_count++ (track reprint count)
  - last_printed_at = NOW, last_printed_by = actor
  - audit log в wms_label_print_log с source='scan_reprint'
Файл: src/app/api/wms/kiz/scan-reprint/route.ts
GET /api/wms/kiz/stats Возвращает КИЗ-counters per-product или total per-state.
GET /api/wms/kiz/stats?product_ids=1,2,3 | by_state=true

Возвращает КИЗ-counters per-product или total per-state.
Используется в product list (badge «КИЗ: 5/8») и dashboard.

- Без params: aggregate по state для ффайла (или клиента-селлера)
- product_ids=1,2,3: per-product breakdown (для product list)
Файл: src/app/api/wms/kiz/stats/route.ts
POST /api/wms/kiz/upload-file Принимает FormData: file (.xlsx | .csv | .txt) + product_id?, client_id?
POST /api/wms/kiz/upload-file
Принимает FormData: file (.xlsx | .csv | .txt) + product_id?, client_id?

Извлекает КИЗ-коды из файла:
  - .txt: одна строка = один код
  - .csv: первая непустая ячейка в каждой строке
  - .xlsx: все ячейки всех листов где попадается строка `01<14digits>21<chars>`

Затем парсит каждый код через parseKiz() и вставляет в wms_kiz
с pre-check на duplicates (uq_wms_kiz_raw_code).

Limit на размер: 25 MB (увеличено для крупных партий маркировок).

Returns { inserted, invalid, skipped, total_extracted, ids }
Файл: src/app/api/wms/kiz/upload-file/route.ts
GET /api/wms/kpi/dashboard 3 ключевых метрики для WMS-дашборда (WB-style KPI-баннеры):
GET /api/wms/kpi/dashboard
3 ключевых метрики для WMS-дашборда (WB-style KPI-баннеры):
  on_time_pct          — % отгрузок завершённых в срок (за 30 дней)
  avg_assembly_minutes — средние минуты от confirmed_at до completed_at
  active_supplies      — count поставок в confirmed/in_progress сейчас

Если selektor (client_id != null) — фильтруем по client_id.
Файл: src/app/api/wms/kpi/dashboard/route.ts
GET /api/wms/kpi/timeseries Метрики временные ряды для charts на dashboard:
GET /api/wms/kpi/timeseries?from=&to=&metric=

Метрики временные ряды для charts на dashboard:
  - shipments_per_day: сколько completed FBS/FBO/DBS-поставок в день
  - inbound_per_day: сколько приёмок в день
  - units_per_day: SUM units по completed заявкам в день
  - storage_volume: snapshots wms_storage_log per day

Возвращает: [{date, shipments, inbound, units, volume_m3}]
Файл: src/app/api/wms/kpi/timeseries/route.ts
GET /api/wms/label-templates (нет описания)
GET /api/wms/label-templates?client_id=
Файл: src/app/api/wms/label-templates/route.ts
POST /api/wms/label-templates body: { name, size_mm, client_id?, layout?, notes?, is_default? }
POST /api/wms/label-templates
body: { name, size_mm, client_id?, layout?, notes?, is_default? }
Файл: src/app/api/wms/label-templates/route.ts
GET /api/wms/label-templates/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/label-templates/[id]/route.ts
PATCH /api/wms/label-templates/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/label-templates/[id]/route.ts
DELETE /api/wms/label-templates/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/label-templates/[id]/route.ts
POST /api/wms/labels/bulk-render Bulk-генерация N этикеток в одном файле / stream.
POST /api/wms/labels/bulk-render

Bulk-генерация N этикеток в одном файле / stream.

Body:
  template_id: required
  contexts: required — array of context objects (placeholder values)
  format: 'zpl' | 'tspl' | 'epl' | 'pdf'
  copies_per_label?: 1  — кол-во копий каждой этикетки

Behaviour по format:
  pdf  — multi-page PDF (один контекст = одна страница)
  zpl/tspl/epl — concatenated stream (^XA...^XZ ^XA...^XZ для ZPL и т.п.)

Используется в FBS work-mode для печати стикеров пачкой.
Max contexts: 500.
Файл: src/app/api/wms/labels/bulk-render/route.ts
POST /api/wms/labels/preview body: { schema, data?: {...} }
POST /api/wms/labels/preview
body: { schema, data?: {...} }

Возвращает SVG preview БЕЗ сохранения template и без audit log.
Используется в designer UI для live-preview при редактировании.

Только SVG — не PDF/ZPL/TSPL (preview live должен быть быстрым).
Файл: src/app/api/wms/labels/preview/route.ts
POST /api/wms/labels/render body: {
POST /api/wms/labels/render
body: {
  template_id,
  data: { ... placeholders ... } OR contexts: [{...}, {...}],
  format: 'zpl' | 'tspl' | 'epl' | 'pdf' | 'svg' | 'png',
  copies?: 1,
  download?: false  // if true → attachment вместо inline
}

Возвращает рендер в выбранном формате + пишет в wms_label_print_log.
Файл: src/app/api/wms/labels/render/route.ts
GET /api/wms/labels/templates (нет описания)
GET /api/wms/labels/templates?kind=&client_id=&archived=
Файл: src/app/api/wms/labels/templates/route.ts
POST /api/wms/labels/templates body: { name, kind, client_id?, width_mm, height_mm, dpi?, orientation?,
POST /api/wms/labels/templates
body: { name, kind, client_id?, width_mm, height_mm, dpi?, orientation?,
        schema, supported_formats?, is_default? }
Файл: src/app/api/wms/labels/templates/route.ts
GET /api/wms/labels/templates/[id] Возвращает template + auto-migrate v1 → v2 при чтении (только в response,
GET /api/wms/labels/templates/[id]

Возвращает template + auto-migrate v1 → v2 при чтении (только в response,
в DB не пишется до явного save через PATCH).
Файл: src/app/api/wms/labels/templates/[id]/route.ts
PATCH /api/wms/labels/templates/[id] Edit template:
PATCH /api/wms/labels/templates/[id]

Edit template:
  - Меняем name/notes/is_default → in-place update (без version bump)
  - Меняем schema → создаём новую row (immutable version), parent_template_id указывает на старую
  - is_archived → soft delete

Body: { name?, notes?, is_default?, schema?, is_archived?, supported_formats? }
Файл: src/app/api/wms/labels/templates/[id]/route.ts
DELETE /api/wms/labels/templates/[id] Soft-delete: is_archived = TRUE. Hard delete только если version=1 и нет parent.
DELETE /api/wms/labels/templates/[id]
Soft-delete: is_archived = TRUE. Hard delete только если version=1 и нет parent.
Файл: src/app/api/wms/labels/templates/[id]/route.ts
GET /api/wms/lookup (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/lookup/route.ts
GET /api/wms/lost-items (нет описания)
GET /api/wms/lost-items — список homeless items.
POST /api/wms/lost-items — report new lost item.
Файл: src/app/api/wms/lost-items/route.ts
POST /api/wms/lost-items (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/lost-items/route.ts
GET /api/wms/lots Query: ?status=expired|expiring_soon|fresh|all (default: all w/o no_expiry)
GET /api/wms/lots
Query: ?status=expired|expiring_soon|fresh|all  (default: all w/o no_expiry)
       ?client_id= ?product_id= ?expiry_only=1

Возвращает остатки по партиям отсортированные FEFO (самые скорые к истечению сверху).
Используется для:
 - "Что скоро портится?" дашборд
 - Pick-листы FEFO
 - Контроль санитарного состояния склада
Файл: src/app/api/wms/lots/route.ts
GET /api/wms/manifests Список manifests текущего ФФ.
GET /api/wms/manifests?status=draft|ready|dispatched|delivered|cancelled
Список manifests текущего ФФ.
Файл: src/app/api/wms/manifests/route.ts
POST /api/wms/manifests body: { driver_id?, driver_name?, driver_phone?, vehicle_plate?,
POST /api/wms/manifests
body: { driver_id?, driver_name?, driver_phone?, vehicle_plate?,
        marketplace?, pvz_id?, destination?, request_ids: string[], notes? }

Создаёт draft manifest и добавляет requests. Total volume/weight/places
вычисляется из request_ids.
Файл: src/app/api/wms/manifests/route.ts
GET /api/wms/manifests/[id] (нет описания)
GET /api/wms/manifests/[id] — detail с items
Файл: src/app/api/wms/manifests/[id]/route.ts
PATCH /api/wms/manifests/[id] body: { status?: 'ready'|'dispatched'|'delivered'|'cancelled', notes?, ... }
PATCH /api/wms/manifests/[id]
body: { status?: 'ready'|'dispatched'|'delivered'|'cancelled', notes?, ... }

State transitions:
  draft → ready → dispatched → delivered
  any → cancelled
Файл: src/app/api/wms/manifests/[id]/route.ts
DELETE /api/wms/manifests/[id] (нет описания)
DELETE /api/wms/manifests/[id] — soft (только если draft)
Файл: src/app/api/wms/manifests/[id]/route.ts
GET /api/wms/marketplace-credentials Список интеграций. credentials НЕ возвращается (security).
GET /api/wms/marketplace-credentials?client_id=
Список интеграций. credentials НЕ возвращается (security).
Файл: src/app/api/wms/marketplace-credentials/route.ts
POST /api/wms/marketplace-credentials body: { client_id, marketplace: 'wb'|'ozon'|'uzum'|'ym', credentials: string }
POST /api/wms/marketplace-credentials
body: { client_id, marketplace: 'wb'|'ozon'|'uzum'|'ym', credentials: string }
Upsert по (client_id, marketplace).

AES-256-GCM: credentials шифруются перед записью через encryptSecret().
Существующий plaintext в БД остаётся читаемым (decryptSecret идемпотентен).
Файл: src/app/api/wms/marketplace-credentials/route.ts
POST /api/wms/marketplace-credentials/[id]/test Проверяет credentials против реального API маркетплейса. На основе результата
POST /api/wms/marketplace-credentials/[id]/test

Проверяет credentials против реального API маркетплейса. На основе результата
обновляет is_valid + last_check_at + last_error.
Файл: src/app/api/wms/marketplace-credentials/[id]/test/route.ts
GET /api/wms/movements (нет описания)
GET /api/wms/movements?type=&product_id=&request_id=&limit=
Файл: src/app/api/wms/movements/route.ts
GET /api/wms/movements/export (нет описания)
GET /api/wms/movements/export?from=&to=&product_id=&type= → CSV
Файл: src/app/api/wms/movements/export/route.ts
POST /api/wms/movements/manual body: {
POST /api/wms/movements/manual
body: {
  type: WmsMovementType,
  product_id: string,
  qty: number (signed: + приход, - расход),
  from_cell_id?, to_cell_id?, box_id?,
  notes?: string
}

Создаёт движение БЕЗ привязки к заявке. Используется для:
  - Корректировок остатка после инвентаризации (type=adjust)
  - Внутренних перемещений между ячейками (type=transfer)
  - Любых нестандартных операций

После insert автоматически refresh stocks view.
Файл: src/app/api/wms/movements/manual/route.ts
POST /api/wms/movements/transfer body: { product_code, qty, from_cell, to_cell, notes? }
POST /api/wms/movements/transfer
body: { product_code, qty, from_cell, to_cell, notes? }

Прямое перемещение единиц товара между двумя ячейками. Создаёт одну запись
в `wms_movement` с type='transfer' + from_cell_id + to_cell_id. Пишет
baseline для будущего полноценного transfer-workflow с заявкой.
Файл: src/app/api/wms/movements/transfer/route.ts
GET /api/wms/news Список опубликованных новостей (ff-specific OR global) + read status текущего юзера.
GET /api/wms/news?include_drafts=1&audience=clients
Список опубликованных новостей (ff-specific OR global) + read status текущего юзера.
Pinned идут первыми, потом по published_at DESC.
Файл: src/app/api/wms/news/route.ts
POST /api/wms/news body: { level?, title, body_md, tags?, audience?, is_pinned?, published_at?, expires_at?, ff_id? }
POST /api/wms/news — создать новость (platform_owner / FF-owner only).
body: { level?, title, body_md, tags?, audience?, is_pinned?, published_at?, expires_at?, ff_id? }

Если publish_now=true → published_at = NOW(). Если ff_id=null и юзер platform_owner — global.
Файл: src/app/api/wms/news/route.ts
GET /api/wms/news/[id] (нет описания)
GET /api/wms/news/[id] — single post
Файл: src/app/api/wms/news/[id]/route.ts
PATCH /api/wms/news/[id] (нет описания)
PATCH /api/wms/news/[id] — admin update
Файл: src/app/api/wms/news/[id]/route.ts
DELETE /api/wms/news/[id] (нет описания)
DELETE /api/wms/news/[id]
Файл: src/app/api/wms/news/[id]/route.ts
POST /api/wms/news/[id]/mark-read UPSERT в wms_news_read.
POST /api/wms/news/[id]/mark-read — отметить пост прочитанным текущим юзером.
UPSERT в wms_news_read.
Файл: src/app/api/wms/news/[id]/mark-read/route.ts
GET /api/wms/news/unread-count Возвращает количество неоткрытых постов (published, не expired, в audience scope).
GET /api/wms/news/unread-count — для бейджа на shell-иконке news.
Возвращает количество неоткрытых постов (published, не expired, в audience scope).
Файл: src/app/api/wms/news/unread-count/route.ts
GET /api/wms/onboarding/state (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/onboarding/state/route.ts
PATCH /api/wms/onboarding/state (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/onboarding/state/route.ts
GET /api/wms/openapi.json Возвращает OpenAPI 3.1 spec для BCP/WMS API.
GET /api/wms/openapi.json
Возвращает OpenAPI 3.1 spec для BCP/WMS API.

Public (нет requireAdmin) — это документация, не данные tenant'а.

Используется:
  - Swagger UI на `/wms/docs/api`
  - Внешние интеграторы (skladbot, OpenAPI client codegen)
  - VS Code REST Client plugins
Файл: src/app/api/wms/openapi.json/route.ts
GET /api/wms/pallet (нет описания)
GET /api/wms/pallet — list pallets (admin/staff view)
Файл: src/app/api/wms/pallet/route.ts
POST /api/wms/pallet (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/pallet/route.ts
POST /api/wms/pallet/[id]/add-box (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/pallet/[id]/add-box/route.ts
GET /api/wms/pdf (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/pdf/route.ts
GET /api/wms/permissions (нет описания)
GET /api/wms/permissions — каталог всех прав (для UI настроек ролей).
Файл: src/app/api/wms/permissions/route.ts
GET /api/wms/picker-stats Query:
GET /api/wms/picker-stats — leaderboard сборщиков.

Query:
  ?period=today | week | month  (default: today)
  ?limit=20

Returns: ranked picker performance с scans/units/avg-time.
Источник — wms_picker_stat_daily MV (refresh daily).
Файл: src/app/api/wms/picker-stats/route.ts
GET /api/wms/products Список товаров с пагинацией + поиск по name/sku + точный поиск по barcode.
GET /api/wms/products?client_id=&search=&barcode=&page=&per_page=
Список товаров с пагинацией + поиск по name/sku + точный поиск по barcode.
Файл: src/app/api/wms/products/route.ts
POST /api/wms/products body: { client_id, sku, name, barcode? }
POST /api/wms/products
body: { client_id, sku, name, barcode? }
Файл: src/app/api/wms/products/route.ts
GET /api/wms/products/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/route.ts
PATCH /api/wms/products/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/route.ts
DELETE /api/wms/products/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/route.ts
GET /api/wms/products/[id]/components (нет описания)
GET /api/wms/products/[id]/components — комплектующие товара
Файл: src/app/api/wms/products/[id]/components/route.ts
POST /api/wms/products/[id]/components body: { component_product_id, qty }
POST /api/wms/products/[id]/components
body: { component_product_id, qty }
Файл: src/app/api/wms/products/[id]/components/route.ts
DELETE /api/wms/products/[id]/components (нет описания)
DELETE /api/wms/products/[id]/components?component_product_id=X
Файл: src/app/api/wms/products/[id]/components/route.ts
GET /api/wms/products/[id]/external Список MP-bindings для product (wb/ozon/ym/uzum).
GET /api/wms/products/[id]/external
Список MP-bindings для product (wb/ozon/ym/uzum).

POST body: { marketplace, external_id, external_sku? }
  Создаёт или обновляет привязку (UPSERT по UNIQUE product_id+marketplace).

DELETE ?marketplace=wb — удалить конкретную привязку.
Файл: src/app/api/wms/products/[id]/external/route.ts
POST /api/wms/products/[id]/external (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/external/route.ts
DELETE /api/wms/products/[id]/external (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/external/route.ts
GET /api/wms/products/[id]/locations (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/[id]/locations/route.ts
GET /api/wms/products/[id]/photos Возвращает массив photos из товара (на случай если list эндпоинт его не вернёт).
GET /api/wms/products/[id]/photos
Возвращает массив photos из товара (на случай если list эндпоинт его не вернёт).
Файл: src/app/api/wms/products/[id]/photos/route.ts
POST /api/wms/products/[id]/photos multipart/form-data, поле 'file'
POST /api/wms/products/[id]/photos
multipart/form-data, поле 'file'

Сохраняет файл в public/uploads/wms/products/{client_id}/{product_id}/{uuid}.{ext}
Добавляет в wms_product.photos JSONB array. Max 8 фото / 5 MB / jpeg|png|webp.
Файл: src/app/api/wms/products/[id]/photos/route.ts
PATCH /api/wms/products/[id]/photos/[photo_id] body: { action: 'set_main' }
PATCH /api/wms/products/[id]/photos/[photo_id]
body: { action: 'set_main' }
Делает фото первым в массиве (sort_order=0) + обновляет image_url.
Файл: src/app/api/wms/products/[id]/photos/[photo_id]/route.ts
DELETE /api/wms/products/[id]/photos/[photo_id] Удаляет фото из JSONB и физически с диска.
DELETE /api/wms/products/[id]/photos/[photo_id]
Удаляет фото из JSONB и физически с диска.
Если удаляется первое — обновляет image_url.
Файл: src/app/api/wms/products/[id]/photos/[photo_id]/route.ts
POST /api/wms/products/[id]/photos/sync-from-source Скачивает ВСЕ фото товара с внешнего CDN по pattern URL.
POST /api/wms/products/[id]/photos/sync-from-source

Скачивает ВСЕ фото товара с внешнего CDN по pattern URL.
Например WB CDN basket-XX.wbbasket.ru/.../1.webp → пробует 1.webp, 2.webp,
3.webp... до 2 подряд 404 или max 12 фото.

Использует image_url товара как seed. Если main фото уже в photos[],
начинает со следующего индекса. Лимит — 12 фото на товар.
Файл: src/app/api/wms/products/[id]/photos/sync-from-source/route.ts
GET /api/wms/products/[id]/stocks-detail Возвращает 8-counter multi-stock + KIZ availability per product.
GET /api/wms/products/[id]/stocks-detail

Возвращает 8-counter multi-stock + KIZ availability per product.
Multi-tenant safe — JOIN на ff_id.
Файл: src/app/api/wms/products/[id]/stocks-detail/route.ts
POST /api/wms/products/[id]/sync-from-mp Подтянуть актуальные данные карточки товара из marketplace.
POST /api/wms/products/[id]/sync-from-mp?force=1&marketplace=wb

Подтянуть актуальные данные карточки товара из marketplace.

Условия:
  - У товара external_marketplace_id (= WB nmID, Ozon product_id, YM offer/sku)
  - У клиента есть валидный credential для соответствующего MP

Поддержка marketplace'ов:
  - **WB** (полный): name, brand, category, dimensions, photos, characteristics
    через `enrichProductFromWbCard` + Content API
  - **Ozon** (basic): name, barcode, primary_image через product/info/list
  - **YM** (basic): name, image, barcode через offer_mapping_entries

Query param `marketplace`: явно указать какой MP синхронизировать (если у
клиента несколько). По умолчанию — первый валидный credential.
Файл: src/app/api/wms/products/[id]/sync-from-mp/route.ts
PATCH /api/wms/products/bulk (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/bulk/route.ts
POST /api/wms/products/bulk-import multipart/form-data: file=<xlsx/csv>
POST /api/wms/products/bulk-import

multipart/form-data: file=<xlsx/csv>

Колонки Excel:
  SKU* | name* | client_name* | barcode | category | price | weight_g | requires_kiz (1/0/true/false)

Strategy: upsert по (ff_id, client_id, sku) — обновляет существующие, создаёт новые.
Возвращает preview с action='create'|'update'|'skip' per строка.
Файл: src/app/api/wms/products/bulk-import/route.ts
POST /api/wms/products/bulk-import/[id]/apply body: { skip_failed?: boolean, mode?: 'create_only' | 'update_only' | 'both' }
POST /api/wms/products/bulk-import/[id]/apply
body: { skip_failed?: boolean, mode?: 'create_only' | 'update_only' | 'both' }

Применяет products bulk-import. По умолчанию upsert (create + update).
mode controls стратегию: create_only — не трогает существующие;
update_only — не создаёт новые.
Файл: src/app/api/wms/products/bulk-import/[id]/apply/route.ts
GET /api/wms/products/export (нет описания)
GET /api/wms/products/export?client_id= → CSV file
Файл: src/app/api/wms/products/export/route.ts
POST /api/wms/products/import (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/products/import/route.ts
POST /api/wms/products/sync-photos Body: { limit?: number, dry_run?: boolean }
POST /api/wms/products/sync-photos
Body: { limit?: number, dry_run?: boolean }

Запускает batch-загрузку фото товаров текущего ФФ для UI кнопки
«Подгрузить фото» в /wms/products. Скачивает фото только для товаров
с image_url но без photos[].

Доступ: любой owner/manager текущего ФФ (через X-Auth-Token).
Платформенный owner может запустить через cron `?ff_id=N`.
Файл: src/app/api/wms/products/sync-photos/route.ts
GET /api/wms/pvz (нет описания)
GET /api/wms/pvz?marketplace=&active=1&search=
Файл: src/app/api/wms/pvz/route.ts
POST /api/wms/pvz (нет описания)
POST /api/wms/pvz — создать ПВЗ
Файл: src/app/api/wms/pvz/route.ts
GET /api/wms/pvz/[id] (нет описания)
GET /api/wms/pvz/[id]
Файл: src/app/api/wms/pvz/[id]/route.ts
PATCH /api/wms/pvz/[id] (нет описания)
PATCH /api/wms/pvz/[id] — частичное обновление
Файл: src/app/api/wms/pvz/[id]/route.ts
DELETE /api/wms/pvz/[id] (нет описания)
DELETE /api/wms/pvz/[id] — soft delete (is_active = FALSE)
Файл: src/app/api/wms/pvz/[id]/route.ts
GET /api/wms/rejected-events Возвращает rejected scans для текущего user'а (или всех users в tenant если
GET /api/wms/rejected-events?unacked_only=1

Возвращает rejected scans для текущего user'а (или всех users в tenant если
admin). По дефолту только unacked (acked_at IS NULL).

ТСД pull'ит этот endpoint при login + после каждого pendingEvent flush —
показывает оператору badge "X scan'ов отклонено" → screen с деталями.
Файл: src/app/api/wms/rejected-events/route.ts
POST /api/wms/rejected-events body: { ids: string[] }
POST /api/wms/rejected-events/ack
body: { ids: string[] }

Оператор прочитал/закрыл notification → mark acked_at.
Файл: src/app/api/wms/rejected-events/route.ts
POST /api/wms/rejected-events/record (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/rejected-events/record/route.ts
GET /api/wms/reports/by-client Топ клиентов по объёму операций и выручке за период.
GET /api/wms/reports/by-client?days=30

Топ клиентов по объёму операций и выручке за период.
Используется в /wms/reports.
Файл: src/app/api/wms/reports/by-client/route.ts
GET /api/wms/reports/debtors Возвращает список клиентов с непогашенной задолженностью:
GET /api/wms/reports/debtors?days_overdue=14

Возвращает список клиентов с непогашенной задолженностью:
  - total_invoiced: сумма всех выставленных счетов
  - total_paid: сумма всех оплат
  - total_overdue: непогашено по счетам старше N дней
  - oldest_unpaid_age_days: возраст самого старого неоплаченного

Default days_overdue=14.
Файл: src/app/api/wms/reports/debtors/route.ts
GET /api/wms/reports/export Бухгалтерские отчёты для FF:
GET /api/wms/reports/export?kind=revenue|services|stock|invoices&from=YYYY-MM-DD&to=YYYY-MM-DD&format=xlsx|csv

Бухгалтерские отчёты для FF:
  - revenue: invoice total по периоду (по клиентам)
  - services: какие услуги оказывались (с qty/sum)
  - stock: текущие остатки snapshot по клиентам
  - invoices: полный реестр счетов

Возвращает xlsx (по умолчанию) или csv. Файл в attachment.
Файл: src/app/api/wms/reports/export/route.ts
GET /api/wms/reports/finance-summary Финансовый отчёт:
GET /api/wms/reports/finance-summary?days=180&group=month

Финансовый отчёт:
  - выручка по месяцам (timeseries)
  - разбивка по статусу счетов (draft/issued/paid/canceled)
  - топ услуг (по сумме за период)
Файл: src/app/api/wms/reports/finance-summary/route.ts
GET /api/wms/reports/timeseries Возвращает объём операций по бакетам времени.
GET /api/wms/reports/timeseries?metric=requests|movements&group=day|week|month&days=30

Возвращает объём операций по бакетам времени.
Используется для линейных и stacked графиков на дашборде.
Файл: src/app/api/wms/reports/timeseries/route.ts
GET /api/wms/reports/warehouse-load Загрузка складов: % занятых ячеек + общее кол-во штук + резерв.
GET /api/wms/reports/warehouse-load

Загрузка складов: % занятых ячеек + общее кол-во штук + резерв.
Файл: src/app/api/wms/reports/warehouse-load/route.ts
GET /api/wms/request-items/[id]/services Effective_price = COALESCE(override, client_tariff, default_price).
GET /api/wms/request-items/[id]/services — услуги привязанные к позиции заявки.
Effective_price = COALESCE(override, client_tariff, default_price).
Файл: src/app/api/wms/request-items/[id]/services/route.ts
POST /api/wms/request-items/[id]/services body: { service_id, qty, price?, discount_pct?, notes? }
POST /api/wms/request-items/[id]/services
body: { service_id, qty, price?, discount_pct?, notes? }

Idempotent upsert по (request_item_id, service_id) — если такой service уже
есть на этой позиции, инкрементирует qty или замещает price/discount если переданы.
Файл: src/app/api/wms/request-items/[id]/services/route.ts
PATCH /api/wms/request-items/[id]/services/[service_id] id = request_item_id
id = request_item_id
service_id = wms_request_item_service.id (запись связки)

PATCH: обновить qty / price / discount_pct
DELETE: убрать услугу с позиции
Файл: src/app/api/wms/request-items/[id]/services/[service_id]/route.ts
DELETE /api/wms/request-items/[id]/services/[service_id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/request-items/[id]/services/[service_id]/route.ts
GET /api/wms/requests (нет описания)
GET /api/wms/requests?type=&status=
Файл: src/app/api/wms/requests/route.ts
POST /api/wms/requests body: { type, client_id?, warehouse_id?, title?, planned_date?, notes?,
POST /api/wms/requests
body: { type, client_id?, warehouse_id?, title?, planned_date?, notes?,
        items?: [{product_id, planned_qty}] }

Создаёт draft-заявку. Номер генерируется автоматически (TYPE-YYYY-NNNN).
Файл: src/app/api/wms/requests/route.ts
GET /api/wms/requests/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/requests/[id]/route.ts
PATCH /api/wms/requests/[id] Меняет только метаданные. Только в статусе draft.
PATCH /api/wms/requests/[id]
Меняет только метаданные. Только в статусе draft.
Файл: src/app/api/wms/requests/[id]/route.ts
DELETE /api/wms/requests/[id] (нет описания)
DELETE /api/wms/requests/[id] — только draft.
Файл: src/app/api/wms/requests/[id]/route.ts
POST /api/wms/requests/[id]/auto-advance Если все позиции отсканированы (actual_qty >= planned_qty для всех items)
POST /api/wms/requests/[id]/auto-advance

Если все позиции отсканированы (actual_qty >= planned_qty для всех items)
И заявка в статусе in_progress → возвращает recommendation = 'completed'.
Это endpoint для UI-toast «можно передать на доставку» и для workflow worker'а.

Реальный transition не делает — ответственность UI или отдельного auto-worker'а.
Файл: src/app/api/wms/requests/[id]/auto-advance/route.ts
GET /api/wms/requests/[id]/boxes Возвращает коробки прикреплённые к этой партии (FBO supply ↔ box m2m).
GET /api/wms/requests/[id]/boxes
Возвращает коробки прикреплённые к этой партии (FBO supply ↔ box m2m).
Файл: src/app/api/wms/requests/[id]/boxes/route.ts
POST /api/wms/requests/[id]/boxes body: { box_code | box_id } — добавить коробку в партию
POST /api/wms/requests/[id]/boxes
body: { box_code | box_id }  — добавить коробку в партию

Если box_code — ищем box по коду (а штрихкод валятся в ту же логику scanner).
Файл: src/app/api/wms/requests/[id]/boxes/route.ts
DELETE /api/wms/requests/[id]/boxes/[box_id] Открепить коробку от партии (m2m unlink). Сама коробка остаётся в wms_box
DELETE /api/wms/requests/[id]/boxes/[box_id]
Открепить коробку от партии (m2m unlink). Сама коробка остаётся в wms_box
(может быть переиспользована).

Если у связки есть wb_trbx_id (грузоместо синхронизировано с WB) —
параллельно делаем DELETE /api/v3/supplies/{supplyId}/trbx body={ids:[...]}
чтобы убрать грузоместо в кабинете WB. Если WB недоступен / 404 — всё равно
удаляем локально, но в audit пишем wb_error.

После shipped/delivered — отказываем (WB сам в этих статусах не даёт).
Файл: src/app/api/wms/requests/[id]/boxes/[box_id]/route.ts
POST /api/wms/requests/[id]/cargo-places body: { count: number, sync_with_marketplace?: boolean }
POST /api/wms/requests/[id]/cargo-places
body: { count: number, sync_with_marketplace?: boolean }

Массовое создание грузомест (cargo places) для FBS/DBS/FBO поставки.

--- Логика интеграции с WB ---
Если у заявки есть delivery_number вида `WB-GI-...` И marketplace='wb'
И у клиента валидный WB token → ИНТЕГРАЦИЯ ВКЛ:
  1. POST /api/v3/supplies/{WB-GI-xxx}/trbx { amount: count }
     → WB вернёт trbxIds: ['wb-trbx-...', ...]
  2. Локально создаём wms_box на каждый trbx, barcode = trbx_id (WB QR-код)
  3. wms_supply_box.wb_trbx_id = wb-trbx-..., wb_synced_at = NOW()

Иначе (FBS без WB-интеграции / OZON / ручная заявка):
  - создаём boxes локально с кодом `{number}-CP-NN`, barcode = code

При sync_with_marketplace=false — принудительно локально, даже если есть WB.

Идемпотентность: повторный вызов добавляет ДОПОЛНИТЕЛЬНЫЕ места
(WB POST trbx тоже инкрементальный — amount=N добавит ещё N).
Файл: src/app/api/wms/requests/[id]/cargo-places/route.ts
GET /api/wms/requests/[id]/cargo-places/[box_id]/orders body: { request_item_ids: string[] }
POST /api/wms/requests/[id]/cargo-places/[box_id]/orders
body: { request_item_ids: string[] }

Связать заказы (request_items) с конкретным грузоместом (trbx).
 1. Берём external_task_id из выбранных request_items (это WB assembly_ids)
 2. PATCH в WB API: связать с trbx через addOrdersToWbTrbx
 3. Локально создаём wms_box_item для трассировки «какой заказ в каком коробе»
    (ON CONFLICT DO UPDATE — позволяет переложить заказ в другой бокс)

Передача пустого массива {request_item_ids: []} — отвязать все заказы от trbx.

GET — посмотреть какие заказы уже привязаны к этому grouzomesta.
Файл: src/app/api/wms/requests/[id]/cargo-places/[box_id]/orders/route.ts
POST /api/wms/requests/[id]/cargo-places/[box_id]/orders (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/requests/[id]/cargo-places/[box_id]/orders/route.ts
GET /api/wms/requests/[id]/cargo-places/[box_id]/wb-qr Получает QR-код грузоместа из WB API (тот самый который надо печатать на короб).
GET /api/wms/requests/[id]/cargo-places/[box_id]/wb-qr?type=svg|png

Получает QR-код грузоместа из WB API (тот самый который надо печатать на короб).
Используется в /wms/print/box-bulk когда у box есть wb_trbx_id.

Возвращает:
  - 200 image/{svg+xml|png} с body — раскодированный из base64 ответ WB
  - 404 если box не найден или у него нет wb_trbx_id
  - 502 если WB API ругнулся

Тип по умолчанию png. Для печати лучше svg (векторный).
Файл: src/app/api/wms/requests/[id]/cargo-places/[box_id]/wb-qr/route.ts
POST /api/wms/requests/[id]/cargo-places/sync Подтягивает все trbx из WB-поставки и приводит локальное состояние к WB:
POST /api/wms/requests/[id]/cargo-places/sync

Подтягивает все trbx из WB-поставки и приводит локальное состояние к WB:
  - Все WB trbx, которых у нас нет → создаём wms_box + wms_supply_box link
  - Наши grouzomesta с wb_trbx_id, которых уже нет в WB → удаляем link и архивируем
  - Существующие совпадения — без изменений

Аналог «pull from WB» для grouzomest. Если продавец создал трбх в WB-кабинете
(или из другого клиента), эта команда их подтянет к нам.

Возвращает diff: { added: N, removed: M, kept: K, total }.
Файл: src/app/api/wms/requests/[id]/cargo-places/sync/route.ts
GET /api/wms/requests/[id]/consumables Возвращает расходники потраченные на заявку (с остатком на складе).
GET /api/wms/requests/[id]/consumables
Возвращает расходники потраченные на заявку (с остатком на складе).
Файл: src/app/api/wms/requests/[id]/consumables/route.ts
POST /api/wms/requests/[id]/consumables body: { product_id, qty, price?, discount_pct?, notes? }
POST /api/wms/requests/[id]/consumables
body: { product_id, qty, price?, discount_pct?, notes? }
Idempotent upsert по (request_id, product_id) — суммирует qty.
Файл: src/app/api/wms/requests/[id]/consumables/route.ts
PATCH /api/wms/requests/[id]/consumables/[consumable_id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/requests/[id]/consumables/[consumable_id]/route.ts
DELETE /api/wms/requests/[id]/consumables/[consumable_id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/requests/[id]/consumables/[consumable_id]/route.ts
GET /api/wms/requests/[id]/events (нет описания)
GET /api/wms/requests/[id]/events — audit timeline (newest first).
Файл: src/app/api/wms/requests/[id]/events/route.ts
POST /api/wms/requests/[id]/events body: { notes: string }
POST /api/wms/requests/[id]/events — добавить комментарий.
body: { notes: string }
Файл: src/app/api/wms/requests/[id]/events/route.ts
GET /api/wms/requests/[id]/files (нет описания)
GET /api/wms/requests/[id]/files — список файлов заявки.
Файл: src/app/api/wms/requests/[id]/files/route.ts
POST /api/wms/requests/[id]/files Принимает любой MIME (УПД, накладные, фото-приёмки), сохраняет в
POST /api/wms/requests/[id]/files — multipart upload.
Принимает любой MIME (УПД, накладные, фото-приёмки), сохраняет в
public/uploads/wms/requests/{id}/{uuid}.{ext}
Лимит 20 МБ.
Файл: src/app/api/wms/requests/[id]/files/route.ts
DELETE /api/wms/requests/[id]/files/[file_id] Удаляет запись + физически с диска (best-effort).
DELETE /api/wms/requests/[id]/files/[file_id]
Удаляет запись + физически с диска (best-effort).
Файл: src/app/api/wms/requests/[id]/files/[file_id]/route.ts
POST /api/wms/requests/[id]/services-bulk body: {
POST /api/wms/requests/[id]/services-bulk
body: {
  service_id: string,
  qty_mode?: 'per_item' | 'per_unit',  // default per_item
  qty?: number,                          // multiplier (default 1)
  price?: number | null,                 // override (null/undefined → берётся тариф/default)
  discount_pct?: number,                 // 0..100
  notes?: string,
  only_item_ids?: string[],              // если задано — только эти позиции; иначе ВСЕ
}

Массовое добавление одной услуги к нескольким (или всем) позициям заявки.
- per_item: qty услуги = qty (default 1) на КАЖДУЮ позицию
- per_unit: qty услуги = planned_qty товара × qty (полезно для упаковки/маркировки
  когда «1 услуга на штуку», а в позиции 5 шт → 5 услуг)

Идемпотентность та же что у per-item POST: ON CONFLICT (request_item_id, service_id)
— qty инкрементируется. Это даёт «безопасное повторное нажатие» с риском двойного
биллинга, поэтому в UI диалог имеет confirm.
Файл: src/app/api/wms/requests/[id]/services-bulk/route.ts
POST /api/wms/requests/[id]/spawn body: { next_type, copy_items?: boolean, title?: string, notes?: string }
POST /api/wms/requests/[id]/spawn
body: { next_type, copy_items?: boolean, title?: string, notes?: string }

Создаёт связанную заявку (chain) — задаёт prev_request_id = текущая.
По умолчанию копирует client_id, warehouse_id, и items (если copy_items=true).

Используется для UI «Следующие действия» на карточке заявки:
  inbound completed → spawn(processing) для маркировки КИЗ
  inbound completed → spawn(transfer)  для размещения в коробки
  fbs confirmed → spawn(processing) для сборки/упаковки
  any → spawn(<same type>) для дубликата
Файл: src/app/api/wms/requests/[id]/spawn/route.ts
GET /api/wms/requests/[id]/stages (нет описания)
GET /api/wms/requests/[id]/stages — список этапов заявки
Файл: src/app/api/wms/requests/[id]/stages/route.ts
POST /api/wms/requests/[id]/stages Body: { action: 'advance' | 'rollback' | 'assign', stage_id, assignee_id?, data?, notes? }
POST /api/wms/requests/[id]/stages
Body: { action: 'advance' | 'rollback' | 'assign', stage_id, assignee_id?, data?, notes? }

advance: complete current stage и start следующего; SLA-deadline = NOW() + duration_minutes
rollback: вернуть status='in_progress' предыдущего этапа (требует override)
assign: смена исполнителя
Файл: src/app/api/wms/requests/[id]/stages/route.ts
POST /api/wms/requests/[id]/transition body: { to: WmsRequestStatus, items_actual?: { [request_item_id]: actual_qty } }
POST /api/wms/requests/[id]/transition
body: { to: WmsRequestStatus, items_actual?: { [request_item_id]: actual_qty } }

Проверяет state machine. На completion заявок с auto-movement (inbound/fbo/fbs/dbs)
генерирует движения по wms_movement.
Файл: src/app/api/wms/requests/[id]/transition/route.ts
POST /api/wms/requests/[id]/wb-deliver Финальный шаг FBS: «Передать в доставку».
POST /api/wms/requests/[id]/wb-deliver

Финальный шаг FBS: «Передать в доставку».
 1. Валидация: marketplace='wb', delivery_number LIKE 'WB-%', все заказы в supply
 2. PATCH /api/v3/supplies/{id}/deliver через WB API → supply закрыта от
    редактирования, заказы переходят в delivery
 3. Локально status → shipped + audit-event wb_delivered

После этого изменить supply / удалить grouzomesta нельзя (WB rejects).
Если в supply нет ни одного активного заказа — WB вернёт ошибку, не падаем
молча.
Файл: src/app/api/wms/requests/[id]/wb-deliver/route.ts
GET /api/wms/requests/[id]/wb-supply-qr QR-код всей FBS-поставки (WB-GI-...) для печати на машину/паллет/коробку
GET /api/wms/requests/[id]/wb-supply-qr?type=svg|png

QR-код всей FBS-поставки (WB-GI-...) для печати на машину/паллет/коробку
когда отгружаешь в СЦ или на склад WB (без grouzomesta). На ПВЗ нужен QR
каждого trbx, не этот.

Аналог «QR поставки» в WB-кабинете.
Файл: src/app/api/wms/requests/[id]/wb-supply-qr/route.ts
GET /api/wms/requests/bulk-import (нет описания)
GET /api/wms/requests/bulk-import — история импортов tenant'а
Файл: src/app/api/wms/requests/bulk-import/route.ts
POST /api/wms/requests/bulk-import multipart/form-data: file=<xlsx/csv>, options?=<JSON>
POST /api/wms/requests/bulk-import

multipart/form-data: file=<xlsx/csv>, options?=<JSON>

Stage 1: парсит файл → валидирует SKU/client/type → создаёт wms_bulk_import
запись со status='preview' и preview JSON. Не создаёт заявки.

Колонки в Excel/CSV:
  SKU | qty | client_name | type | title | notes

Возвращает: import_id + preview (для отображения)
Файл: src/app/api/wms/requests/bulk-import/route.ts
POST /api/wms/requests/bulk-import/[id]/apply Stage 2: Применяет результаты preview — создаёт заявки. Транзакция:
POST /api/wms/requests/bulk-import/[id]/apply

Stage 2: Применяет результаты preview — создаёт заявки. Транзакция:
либо все ok-rows становятся заявками, либо ничего.

options.skip_failed=true — даже если есть failed rows, применить только ok.
Файл: src/app/api/wms/requests/bulk-import/[id]/apply/route.ts
GET /api/wms/returns Список возвратов FBS текущего ФФ. Сортируем по created_at DESC.
GET /api/wms/returns?status=&client_id=&search=&page=&per_page=

Список возвратов FBS текущего ФФ. Сортируем по created_at DESC.
Файл: src/app/api/wms/returns/route.ts
GET /api/wms/returns/[id] Допустимые status: pending → received → processed | rejected | disposed
GET — детали одного возврата.
PATCH — изменить status / reason / cell_id / notes.

Допустимые status: pending → received → processed | rejected | disposed
Файл: src/app/api/wms/returns/[id]/route.ts
PATCH /api/wms/returns/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/returns/[id]/route.ts
POST /api/wms/returns/[id]/classify body: { classification: 'restock'|'repair'|'dispose'|'reject', cell_id?, notes?, photo_urls? }
POST /api/wms/returns/[id]/classify
body: { classification: 'restock'|'repair'|'dispose'|'reject', cell_id?, notes?, photo_urls? }

Полный RMA workflow финал — после приёмки оператор решает:
  - restock → товар возвращается на основной склад (+qty_available)
  - repair → в ремонтный (+qty_repair)
  - dispose → в брак (+qty_defective)
  - reject → отказ принять возврат (без movement)

Создаёт wms_movement type='receive' с notes 'RMA-{classification}' и
привязывает к wms_return.movement_id. Меняет status='processed' и
processed_at=NOW().

Wave 33 addition: FBO loop closure — qty_mp ↩
  - При любой классификации кроме 'reject': декрементирует wms_stock.qty_mp
    на qty возврата (saturating to 0 через GREATEST).
  - Вставляет wms_storage_event event_type='fbo_returned' (+qty_delta).
  - Если qty_mp был 0 (возврат без предварительной отгрузки через BCP) —
    qty_mp остаётся 0 (GREATEST защита), в notes фиксируется underflow.

Idempotency: если return уже processed — возвращает 409.
Файл: src/app/api/wms/returns/[id]/classify/route.ts
POST /api/wms/returns/[id]/photos multipart/form-data, field 'file'
POST /api/wms/returns/[id]/photos
multipart/form-data, field 'file'

Сохраняет файл в public/uploads/wms/returns/{return_id}/{uuid}.{ext}
и добавляет URL в wms_return.photo_urls JSONB array.

Поддерживается до 10 фото, до 5 МБ, jpeg/png/webp.

Use-case: фотофиксация состояния возвращённого товара перед classification.
Файл: src/app/api/wms/returns/[id]/photos/route.ts
DELETE /api/wms/returns/[id]/photos body: { url: string }
DELETE /api/wms/returns/[id]/photos
body: { url: string }
Удаляет URL из photo_urls (физический файл оставляем — потом cleanup-cron).
Файл: src/app/api/wms/returns/[id]/photos/route.ts
GET /api/wms/returns/[id]/qc Записать результат QC inspection для одной позиции возврата.
POST /api/wms/returns/{id}/qc

Записать результат QC inspection для одной позиции возврата.

Body: { request_item_id, condition, next_action?, photo_proof_b64?, notes? }

Multi-stage flow:
  received → inspecting (this endpoint) → next_action (restock/write_off/return_to_seller)

Photo proof: optional base64 thumbnail (cap 200KB как в inbound/receive).
/
const QcSchema = z.object({
  request_item_id: z.union([z.string(), z.number()]),
  condition: z.enum(['good', 'defective', 'broken_kiz', 'return_to_seller']),
  next_action: z.enum(['restock', 'write_off', 'return_to_seller', 'repair']).optional(),
  photo_proof_b64: z.string().max(200_000).optional(),
  notes: z.string().max(2000).optional(),
});

export async function POST(req: Request, { params }: RouteParams) {
  const r = await getResolvedTenant(req);
  if (!r) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });

  const { id: requestId } = await params;
  const body = await req.json().catch(() => ({}));
  const parsed = QcSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json(
      { error: 'validation_failed', issues: parsed.error.issues },
      { status: 400 },
    );
  }
  const data = parsed.data;
  const actorId = await getActorId(req);

  // Default next_action by condition если не указан
  const nextAction = data.next_action ?? (
    data.condition === 'good' ? 'restock' :
    data.condition === 'defective' ? 'write_off' :
    data.condition === 'broken_kiz' ? 'write_off' :
    'return_to_seller'
  );

  // Verify request exists + tenant scope + type=return (or similar)
  const reqRow = await wmsQuery<{ id: string; type: string }>(
    `SELECT id::text, type::text FROM wms_request WHERE id = $1 AND ff_id = $2`,
    [requestId, r.ff_id],
  );
  if (reqRow.length === 0) {
    return NextResponse.json({ error: 'request_not_found' }, { status: 404 });
  }

  const result = await wmsTx(async (client) => {
    const ins = await client.query<{ id: string }>(
      `INSERT INTO wms_return_qc
         (ff_id, request_id, request_item_id, inspected_by,
          condition, photo_proof_b64, notes, next_action)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
       RETURNING id::text`,
      [
        r.ff_id, requestId, String(data.request_item_id), actorId,
        data.condition,
        data.photo_proof_b64 ?? null,
        data.notes ?? null,
        nextAction,
      ],
    );

    // Update request_item.task_status to reflect QC result
    await client.query(
      `UPDATE wms_request_item
          SET task_status = $1,
              updated_at = NOW()
        WHERE id = $2`,
      [data.condition, String(data.request_item_id)],
    );

    return { id: ins.rows[0].id, condition: data.condition, next_action: nextAction };
  });

  void audit(req, 'return_qc.inspect', {
    entity_table: 'wms_return_qc',
    entity_id: result.id,
    summary: `QC: ${data.condition} → ${nextAction} for request_item ${data.request_item_id}`,
  });

  return NextResponse.json(result);
}

/**
GET /api/wms/returns/{id}/qc — get QC history for этого return request.
Файл: src/app/api/wms/returns/[id]/qc/route.ts
POST /api/wms/returns/[id]/qc (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/returns/[id]/qc/route.ts
POST /api/wms/returns/[id]/unclassify Reverses the last classification of a wms_return. Allows operators to
POST /api/wms/returns/[id]/unclassify

Reverses the last classification of a wms_return. Allows operators to
correct a misclassification (wrong button click) without manual SQL.

Idempotency: returns 409 if status != 'processed' (or 'disposed').
A return that is already 'received' has nothing to undo.

Compensating-movement pattern:
  - The original wms_movement is NOT deleted or updated (audit trail).
  - A new NEGATIVE wms_movement is inserted with notes 'UNCLASSIFY:…'.
  - wms_stock counters are reversed (mirror of classify logic):
      restock   → qty_available  -qty  (was +qty on classify)
      repair    → qty_repair     -qty, qty_available +qty  (was +qty_repair on classify)
      dispose   → qty_defective  -qty, qty_available +qty  (was +qty_defective on classify)
  - qty_mp is restored: +qty  (was decremented on classify via GREATEST)
  - wms_storage_event: event_type='return_unclassified', qty_delta = -qty (negative)
  - wms_return: status='received', processed_at=NULL, movement_id=NULL,
    classification=NULL, classified_at=NULL, classified_by=NULL

RBAC: manager+ (rejectSeller blocks clients; managers and above can reverse).
Multi-tenant: all SQL filtered by ff_id from session.

Returns: { ok: true, return_id: string, restored_status: 'received' }
Файл: src/app/api/wms/returns/[id]/unclassify/route.ts
GET /api/wms/seller-warehouses Список складов селлера на стороне маркетплейса (для фильтра в FBS).
GET /api/wms/seller-warehouses?client_id=&marketplace=

Список складов селлера на стороне маркетплейса (для фильтра в FBS).
У клиента может быть несколько: «Коледино Москва», «Волгоград» и т.д.

Селлер видит только свои; админ может фильтровать по client_id.
Файл: src/app/api/wms/seller-warehouses/route.ts
POST /api/wms/send-document (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/send-document/route.ts
POST /api/wms/sentry-webhook Принимает alerts от Sentry → форматирует → шлёт в Telegram владельцам ФФ.
POST /api/wms/sentry-webhook

Принимает alerts от Sentry → форматирует → шлёт в Telegram владельцам ФФ.

Setup в Sentry:
  Settings → Integrations → Custom Integrations → Webhook
  URL: https://<host>/api/wms/sentry-webhook
  Events: issue.alert, issue.resolved (можно расширить)

Security: проверяем HMAC-SHA256 signature через `Sentry-Hook-Signature`
header против shared secret `SENTRY_WEBHOOK_SECRET` (выставляется в Sentry
Integration settings и в .env.local).

Кому шлём:
  Всем wms_user с role='owner' и заполненным telegram_chat_id.
  Если telegram_chat_id ни у кого не задан — silently skip.
Файл: src/app/api/wms/sentry-webhook/route.ts
GET /api/wms/services (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/services/route.ts
POST /api/wms/services (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/services/route.ts
GET /api/wms/settings/company Возвращает counterparty (юр.лицо/ИП) текущего ФФ. Если у ФФ нет
GET /api/wms/settings/company

Возвращает counterparty (юр.лицо/ИП) текущего ФФ. Если у ФФ нет
counterparty — создаём stub с именем ФФ, чтобы UI имел что
редактировать (counterparty_id NULL → 404, fail loudly).
Файл: src/app/api/wms/settings/company/route.ts
PATCH /api/wms/settings/company body: любые из полей CounterpartyRow
PATCH /api/wms/settings/company
body: любые из полей CounterpartyRow

Обновляет counterparty текущего ФФ. Только owner или platform_owner.
Файл: src/app/api/wms/settings/company/route.ts
GET /api/wms/settings/max Derive the per-tenant MAX webhook receiver URL.
Derive the per-tenant MAX webhook receiver URL.
Multi-tenant: ff_id is embedded in the path so MAX knows which tenant to notify.
/
function webhookUrl(ffId: number): string {
  return `${APP_BASE_URL}/api/webhooks/max/${ffId}`;
}

/**
GET /api/wms/settings/max
Возвращает public-info о текущей настройке MAX-бота ФФ.
Сам токен НИКОГДА не возвращается (в БД он зашифрован).

Дополнительно: опционально проверяет webhook_status через MAX API
(только если токен настроен; best-effort — ошибка → 'unknown').
Результат не кешируется на сервере — клиентский UI решает, как часто опрашивать.
Файл: src/app/api/wms/settings/max/route.ts
PATCH /api/wms/settings/max body: { token: string }
PATCH /api/wms/settings/max
body: { token: string }

Установить/обновить токен MAX-бота для текущего ФФ.
Валидируем через botapi.max.ru/me → шифруем AES-256-GCM → сохраняем.
После сохранения: регистрируем webhook URL в MAX Bot API (best-effort).
Возвращает { ok, username, name, webhook_registered }.
Файл: src/app/api/wms/settings/max/route.ts
DELETE /api/wms/settings/max Удалить custom MAX-токен ФФ — вернуться на платформенный fallback.
DELETE /api/wms/settings/max
Удалить custom MAX-токен ФФ — вернуться на платформенный fallback.
Перед удалением: отменяем webhook subscription в MAX Bot API (best-effort).
Файл: src/app/api/wms/settings/max/route.ts
POST /api/wms/settings/max/reregister-webhook Re-register the MAX webhook URL with the MAX Bot API using the existing
POST /api/wms/settings/max/reregister-webhook

Re-register the MAX webhook URL with the MAX Bot API using the existing
stored token. Used when the webhook is found to be 'missing' (e.g. after
MAX API reset, infrastructure move, or token rotation).

Does NOT re-validate the token against MAX API — uses the already-saved
encrypted token. If the token is invalid, MAX API will return an error.

Returns { ok, webhook_registered }.
Файл: src/app/api/wms/settings/max/reregister-webhook/route.ts
GET /api/wms/settings/telegram Возвращает public-info о текущей настройке бота ФФ.
GET /api/wms/settings/telegram
Возвращает public-info о текущей настройке бота ФФ.
Сам токен НИКОГДА не возвращаем (в БД он зашифрован, в API только маскируется).
Файл: src/app/api/wms/settings/telegram/route.ts
PATCH /api/wms/settings/telegram body: { token }
PATCH /api/wms/settings/telegram
body: { token }

Установить/обновить токен бота для текущего ФФ.
Валидируем через Bot API getMe → шифруем AES-256-GCM → сохраняем.
Возвращает { username, name } полученные от Telegram.
Файл: src/app/api/wms/settings/telegram/route.ts
DELETE /api/wms/settings/telegram Удалить custom-токен ФФ — вернуться на платформенный fallback.
DELETE /api/wms/settings/telegram
Удалить custom-токен ФФ — вернуться на платформенный fallback.
Файл: src/app/api/wms/settings/telegram/route.ts
POST /api/wms/settings/telegram/test body: { chat_id, text? }
POST /api/wms/settings/telegram/test
body: { chat_id, text? }

Отправить тестовое сообщение через бот ФФ. Используется в UI настройки —
чтобы убедиться что бот добавлен в чат и chat_id правильный.
Файл: src/app/api/wms/settings/telegram/test/route.ts
POST /api/wms/signup body: { email, password, full_name?, company_name?, tariff_slug?='seller-free' }
POST /api/wms/signup
body: { email, password, full_name?, company_name?, tariff_slug?='seller-free' }

Public endpoint (см. middleware allowlist) — self-service signup для free-tier.

Логика:
  1. Validate email + password (>= 8 chars)
  2. Resolve tariff (default seller-free)
  3. Transaction:
     a. Create platform_user with email + bcrypt(password)
     b. Create wms_ff (tenant) с именем company_name OR email
     c. Create wms_user привязанный к platform_user.id + ff_id
     d. Create wms_subscription со status='trial', tariff_id=resolved
  4. Return: ok + redirect_to=/login

MINIMAL SCAFFOLD: реальная активация через email требуется (TODO Phase 2).
Для MVP — сразу status='active' (без email verification).
Файл: src/app/api/wms/signup/route.ts
POST /api/wms/slotting/refresh Refresh slotting materialized views (`wms_product_velocity_class` +
POST /api/wms/slotting/refresh

Refresh slotting materialized views (`wms_product_velocity_class` +
`wms_product_affinity`). Admin-only — обычно дёргается cron'ом раз в сутки
через `curl -X POST -H "X-Auth-Token: $ADMIN_TOKEN" ...`.

CONCURRENTLY refresh (если индекс уникальный есть) — без блокировки чтения.
Возвращает счётчики строк в MV после refresh.
Файл: src/app/api/wms/slotting/refresh/route.ts
GET /api/wms/slotting/suggest Smart putaway / slotting AI — приоритизирует ячейки по композитному скору:
GET /api/wms/slotting/suggest?product_id=&warehouse_id=&qty=&limit=

Smart putaway / slotting AI — приоритизирует ячейки по композитному скору:

  score = 0.50 * consolidation_match    (товар уже в ячейке)
        + 0.20 * velocity_match         (A-class → ближе к peak-zone)
        + 0.15 * affinity_match         (товар часто co-pickaется с тем, что в ячейке)
        + 0.15 * capacity_match         (пустая/полу-пустая ячейка)

Возвращает top-N ячеек с разбивкой по факторам — ТСД UI может показать
«положи в A-1-3-2 (score 0.92, потому что: уже есть 3 шт. + velocity A + рядом)».

Multi-tenant: getResolvedTenant() → ff_id. ВСЕ subqueries фильтруют по ff_id.
Файл: src/app/api/wms/slotting/suggest/route.ts
GET /api/wms/stats Один запрос, все счётчики через подзапросы.
GET /api/wms/stats — агрегаты для дашборда.
Один запрос, все счётчики через подзапросы.
Файл: src/app/api/wms/stats/route.ts
DELETE /api/wms/stillages/[id] Полное удаление стеллажа со всеми этажами и ячейками (CASCADE).
DELETE /api/wms/stillages/[id]
Полное удаление стеллажа со всеми этажами и ячейками (CASCADE).

NB: hard-delete потому что архивных стеллажей не существует — они либо есть,
либо нет. На остатках это сейчас не отражается (нет таблицы остатков пока).
Файл: src/app/api/wms/stillages/[id]/route.ts
POST /api/wms/stillages/[id]/copy body: { count: number (1-50), prefix?: string }
POST /api/wms/stillages/[id]/copy
body: { count: number (1-50), prefix?: string }

Bulk-копирование структуры стеллажа: создаёт N новых стеллажей с такими
же этажами и ячейками. Адреса ячеек получают суффикс копии (А-1-2-3 →
А-2-1-2-3 для копии #2).

Атомарно — либо все N созданы, либо ни одного.
Файл: src/app/api/wms/stillages/[id]/copy/route.ts
GET /api/wms/stillages/[id]/structure Возвращает полную схему стеллажа: floors → cells, с подсчётом
GET /api/wms/stillages/[id]/structure

Возвращает полную схему стеллажа: floors → cells, с подсчётом
boxes/units в каждой ячейке. Для рендера схемы в карте склада.
Файл: src/app/api/wms/stillages/[id]/structure/route.ts
GET /api/wms/stockout/alerts Источник — wms_stockout_alert (refresh daily через
GET /api/wms/stockout/alerts — predicted stockout alerts.

Источник — wms_stockout_alert (refresh daily через
/api/cron/stockout-predict). Сортировка по projected_days ASC (ближайший first).
Файл: src/app/api/wms/stockout/alerts/route.ts
GET /api/wms/stocks Производные остатки (из materialized view wms_stock_by_product).
GET /api/wms/stocks?client_id=&product_id=&page=&per_page=
Производные остатки (из materialized view wms_stock_by_product).
Файл: src/app/api/wms/stocks/route.ts
POST /api/wms/stocks (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/stocks/route.ts
POST /api/wms/stocks/bulk-import multipart/form-data: file=<xlsx/csv>
POST /api/wms/stocks/bulk-import

multipart/form-data: file=<xlsx/csv>

Excel columns:
  sku* | client_name* | qty* | cell_address | warehouse_name

Создаёт movements типа 'adjustment_increase' для начальных остатков ФФ при
онбординге (BLOCKER #1). Реальный stock пересчитывается через `wms_stock`
триггер на movement INSERT.

НЕ применяет напрямую — только preview. Apply через
  POST /api/wms/stocks/bulk-import/[id]/apply
Файл: src/app/api/wms/stocks/bulk-import/route.ts
POST /api/wms/stocks/bulk-import/[id]/apply Создаёт wms_movement type='adjustment_increase' для каждой preview row.
POST /api/wms/stocks/bulk-import/[id]/apply

Создаёт wms_movement type='adjustment_increase' для каждой preview row.
Триггер на wms_movement пересчитывает wms_stock автоматически.

notes = `Bulk-import #${id} — начальные остатки`.
Файл: src/app/api/wms/stocks/bulk-import/[id]/apply/route.ts
POST /api/wms/stocks/import (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/stocks/import/route.ts
POST /api/wms/subscriptions body: { tariff_id, billing_cycle?: 'monthly'|'yearly', payment_method?: 'card'|'invoice'|'manual' }
POST /api/wms/subscriptions
body: { tariff_id, billing_cycle?: 'monthly'|'yearly', payment_method?: 'card'|'invoice'|'manual' }

Подписка tenant'а на тариф. MVP: создаёт запись со status='trial', payment_method='manual'.
Реальная оплата — отдельный pipeline (Yookassa/Tinkoff Pay).

Если уже есть активная подписка — она cancellable→cancelled, новая active с
started_at=NOW(), expires_at=NOW()+cycle.
Файл: src/app/api/wms/subscriptions/route.ts
GET /api/wms/subscriptions/current Если подписки нет → возвращаем default (free).
GET /api/wms/subscriptions/current — текущая подписка tenant'а.
Если подписки нет → возвращаем default (free).
Файл: src/app/api/wms/subscriptions/current/route.ts
POST /api/wms/subscriptions/webhook Yookassa / Tinkoff Pay webhook entrypoint.
POST /api/wms/subscriptions/webhook

Yookassa / Tinkoff Pay webhook entrypoint.
Body schema зависит от провайдера — у Yookassa:
  { event: 'payment.succeeded', object: { id, amount: { value, currency }, metadata: { subscription_id } } }

SCAFFOLD: для MVP принимаем только Yookassa-формат. Verify HMAC signature
через `Authorization` header (TODO с реальной интеграцией).

Логика:
  1. Parse event type
  2. Найти subscription по metadata.subscription_id
  3. INSERT wms_subscription_payment с status маппинг (succeeded/failed)
  4. Если succeeded: продлить subscription expires_at + status='active'

Public endpoint (добавляется в middleware allowlist отдельно при подключении).
Файл: src/app/api/wms/subscriptions/webhook/route.ts
POST /api/wms/support/ticket body: { subject: string, message: string, contact_email?: string }
POST /api/wms/support/ticket
body: { subject: string, message: string, contact_email?: string }

Customer support обращение. Минимальный flow без SMTP:
  1. Записываем в wms_audit_log с action='support.ticket.created'
  2. Шлём alert в Telegram канал platform_owner (если настроен)
  3. Возвращаем `{ok, ticket_id}` — UI показывает "Мы получили ваш запрос"

TODO (when SMTP env configured):
  - sendEmail('help@5st.pro', ...) с copy на support team
  - Email confirmation to user

Тикет-система с persistent storage (`wms_support_ticket` table) — отдельной
миграцией когда volume оправдает.
Файл: src/app/api/wms/support/ticket/route.ts
GET /api/wms/tariffs Публичный (auth не нужен) список тарифов для self-service выбора.
GET /api/wms/tariffs?target=seller|ff
Публичный (auth не нужен) список тарифов для self-service выбора.
Файл: src/app/api/wms/tariffs/route.ts
POST /api/wms/transfer (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/transfer/route.ts
POST /api/wms/transfer/[id]/scan (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/transfer/[id]/scan/route.ts
POST /api/wms/tsd/fbo/scan-bind (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/tsd/fbo/scan-bind/route.ts
GET /api/wms/users (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/route.ts
POST /api/wms/users (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/route.ts
GET /api/wms/users/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/[id]/route.ts
POST /api/wms/users/[id] TRUE если в pin_hash лежит bcrypt-хеш — UI рендерит "PIN установлен". */
TRUE если в pin_hash лежит bcrypt-хеш — UI рендерит "PIN установлен". */
  has_pin: boolean;
  created_at: string;
}

export async function GET(req: Request, { params }: RouteParams) {
  try {
    const { id } = await params;
    const ffId = await getTenantId(req);
    const [user] = await wmsQuery<UserDetail>(
      `SELECT u.id::text, u.full_name, u.phone, u.email, u.telegram_username, u.telegram_chat_id,
              u.role::text, u.status, u.client_id::text, c.name AS client_name,
              (u.pin_hash IS NOT NULL) AS has_pin,
              u.created_at
         FROM wms_user u
    LEFT JOIN wms_client c ON c.id = u.client_id
        WHERE u.id = $1 AND u.ff_id = $2`,
      [id, ffId]
    );
    if (!user) return NextResponse.json({ error: 'not_found' }, { status: 404 });
    const permissions = await getUserPermissions(Number(id));
    return NextResponse.json({ ...user, permissions });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return NextResponse.json({ error: 'wms_db_error', message }, { status: 500 });
  }
}

export async function PATCH(req: Request, { params }: RouteParams) {
  try {
    const { id } = await params;
    const ffId = await getTenantId(req);
    const body = await req.json().catch(() => ({}));

    // RBAC: только owner/admin может менять role/status других юзеров. Самостоятельная
    // эскалация (storekeeper → owner) обнаружена в E2E 2026-05-15 → privilege escalation fix.
    const actorId = await getActorId(req);
    const isAdmin = await actorIsAdmin(req);
    const targetIsSelf = String(actorId) === String(id);
    const wantsRoleChange = 'role' in body || 'status' in body || 'client_id' in body;
    if (wantsRoleChange && !isAdmin) {
      return NextResponse.json(
        { error: 'forbidden', message: 'Только owner/admin может менять role, status, client_id' },
        { status: 403 },
      );
    }
    if (!isAdmin && !targetIsSelf) {
      return NextResponse.json(
        { error: 'forbidden', message: 'Можно править только свой профиль' },
        { status: 403 },
      );
    }

    const updates: string[] = [];
    const values: unknown[] = [];
    let i = 1;
    // Простые текстовые поля (без специальной обработки).
    for (const f of ['full_name', 'phone', 'email', 'telegram_username', 'role', 'status'] as const) {
      if (f in body) {
        updates.push(`${f} = ${i++}`);
        const v = body[f];
        values.push(typeof v === 'string' && v.trim() ? v.trim() : null);
      }
    }
    if ('client_id' in body) {
      updates.push(`client_id = ${i++}`);
      values.push(body.client_id != null ? String(body.client_id) : null);
    }
    // PIN — отдельная ветка: хешируем bcrypt'ом и пишем в pin_hash.
    // Plain `pin` колонка считается legacy (см. миграцию 053-clear-plain-pins.sql)
    // и всегда зануляется чтобы не оставлять PIN в открытом виде.
    if ('pin' in body) {
      const raw = body.pin == null ? '' : String(body.pin).trim();
      if (raw === '') {
        // Сброс PIN — терминал-логин больше невозможен для этого пользователя.
        updates.push(`pin_hash = NULL`);
        updates.push(`pin = NULL`);
      } else {
        if (!/^\d{4,8}$/.test(raw)) {
          return NextResponse.json(
            { error: 'invalid_pin_format', message: 'PIN: 4–8 цифр' },
            { status: 400 },
          );
        }
        const hash = await hashPassword(raw);
        updates.push(`pin_hash = ${i++}`);
        values.push(hash);
        updates.push(`pin = NULL`);
      }
    }
    if (updates.length === 0) {
      return NextResponse.json({ error: 'nothing_to_update' }, { status: 400 });
    }
    // Snapshot для audit
    const [beforeUser] = await wmsQuery<{
      email: string; full_name: string | null; role: string; status: string;
    }>(
      `SELECT email, full_name, role, status FROM wms_user WHERE id = $1 AND ff_id = $2`,
      [id, ffId]
    );
    values.push(id);
    values.push(ffId);
    await wmsQuery(
      `UPDATE wms_user SET ${updates.join(', ')} WHERE id = ${i} AND ff_id = ${i + 1}`,
      values
    );
    // B4 audit
    if (beforeUser) {
      const changedFields = Object.keys(body).filter((k) =>
        ['full_name', 'phone', 'email', 'telegram_username', 'role', 'status', 'client_id', 'pin'].includes(k)
      );
      const action = body.status === 'blocked' ? 'user.block'
        : body.status === 'active' && beforeUser.status === 'blocked' ? 'user.unblock'
        : 'user.update';
      void audit(req, action, {
        entity_table: 'wms_user',
        entity_id: id,
        summary: `${action === 'user.block' ? 'Заблокирован' : action === 'user.unblock' ? 'Разблокирован' : 'Изменён'} юзер ${beforeUser.email}: ${changedFields.join(', ')}`,
        before: beforeUser,
        after: { changed: changedFields },
      });
    }
    return NextResponse.json({ ok: true });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return NextResponse.json({ error: 'wms_db_error', message }, { status: 500 });
  }
}

/**
POST /api/wms/users/[id]/permission — установить override.
body: { permission: string, granted: boolean | null }
  granted=true → grant override
  granted=false → revoke override
  granted=null → удалить override (вернуть к роле default)
Файл: src/app/api/wms/users/[id]/route.ts
PATCH /api/wms/users/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/[id]/route.ts
GET /api/wms/users/[id]/companies Один user может быть привязан к нескольким клиентам (юр.лицам).
GET/POST/DELETE — m2m wms_user_company_link для multi-company access.
Один user может быть привязан к нескольким клиентам (юр.лицам).
Файл: src/app/api/wms/users/[id]/companies/route.ts
POST /api/wms/users/[id]/companies (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/[id]/companies/route.ts
DELETE /api/wms/users/[id]/companies (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/users/[id]/companies/route.ts
GET /api/wms/users/[id]/roles Multi-role: user может иметь несколько ролей одновременно (например
Multi-role: user может иметь несколько ролей одновременно (например
'manager' + 'accountant').
Файл: src/app/api/wms/users/[id]/roles/route.ts
PUT /api/wms/users/[id]/roles (нет описания)
PUT body: { roles: string[] } — replace all
Файл: src/app/api/wms/users/[id]/roles/route.ts
GET /api/wms/users/kpi KPI сотрудников ФФ за период. Агрегируем wms_request_event по actor_id
GET /api/wms/users/kpi?from=YYYY-MM-DD&to=YYYY-MM-DD&user_id=

KPI сотрудников ФФ за период. Агрегируем wms_request_event по actor_id
и event_type, маппим events на роли:
  - picker  ← scan_started, scanned (сборка)
  - packer  ← cargo_places_created, cargo_place_orders_set (упаковка)
  - receiver ← transitioned (когда заявка inbound и переходит в received)
  - shipper ← wb_delivered, transitioned to shipped (отгрузка)

Default period: текущий месяц.
Файл: src/app/api/wms/users/kpi/route.ts
GET /api/wms/warehouses Возвращает список складов + total.
GET /api/wms/warehouses
Возвращает список складов + total.
Файл: src/app/api/wms/warehouses/route.ts
POST /api/wms/warehouses body: { name: string, address?: string }
POST /api/wms/warehouses
body: { name: string, address?: string }
Файл: src/app/api/wms/warehouses/route.ts
GET /api/wms/warehouses/[id] (нет описания)
GET /api/wms/warehouses/[id] — карточка склада.
Файл: src/app/api/wms/warehouses/[id]/route.ts
PATCH /api/wms/warehouses/[id] body: { name?: string, address?: string|null, status?: 'active'|'suspended'|'archived' }
PATCH /api/wms/warehouses/[id]
body: { name?: string, address?: string|null, status?: 'active'|'suspended'|'archived' }
Файл: src/app/api/wms/warehouses/[id]/route.ts
DELETE /api/wms/warehouses/[id] Soft-delete: status='archived'.
DELETE /api/wms/warehouses/[id]
Soft-delete: status='archived'.
Файл: src/app/api/wms/warehouses/[id]/route.ts
GET /api/wms/warehouses/[id]/floor-plan (нет описания)
GET /api/wms/warehouses/[id]/floor-plan — текущая карта склада
Файл: src/app/api/wms/warehouses/[id]/floor-plan/route.ts
PUT /api/wms/warehouses/[id]/floor-plan Body: { floor_plan_w_cm, floor_plan_h_cm, floor_plan_grid_cm?,
PUT /api/wms/warehouses/[id]/floor-plan
 Body: { floor_plan_w_cm, floor_plan_h_cm, floor_plan_grid_cm?,
         stillages: [{ id, x_cm, y_cm, w_cm, d_cm, rotation_deg, color_hex }],
         decorations: [{ id?, type, x_cm, y_cm, w_cm, h_cm, rotation_deg, label, color_hex, z_index }] }

 Полный snapshot карты — всё что не в массиве decorations будет удалено.
 Stillages обновляются по id (только координаты, не создаются — они приходят из конструктора).
Файл: src/app/api/wms/warehouses/[id]/floor-plan/route.ts
GET /api/wms/warehouses/[id]/stillages Список стеллажей склада + total_cells (через JOIN на wms_cell).
GET /api/wms/warehouses/[id]/stillages
Список стеллажей склада + total_cells (через JOIN на wms_cell).
Файл: src/app/api/wms/warehouses/[id]/stillages/route.ts
POST /api/wms/warehouses/[id]/stillages body: { code: string, floors: number, cells: number }
POST /api/wms/warehouses/[id]/stillages
body: { code: string, floors: number, cells: number }

Создаёт стеллаж + N этажей + N×M ячеек одной транзакцией.
Адрес ячейки: <code>-<floor:02>-<cell:02>  (A-01-03)
Файл: src/app/api/wms/warehouses/[id]/stillages/route.ts
POST /api/wms/warehouses/import-csv CSV формат:
POST /api/wms/warehouses/import-csv

CSV формат:
  warehouse,stillage,floor,cell,width_cm,length_cm,height_cm,max_weight_kg
  "Основной склад","А","1","1",30,30,40,15
  "Основной склад","А","1","2",30,30,40,15

Логика:
  - warehouse + stillage + floor + cell — резолв или создание иерархии
  - Идемпотентно по (warehouse_id, stillage_code, floor_level, cell_code)

Возвращает: { warehouses_created, stillages_created, floors_created, cells_created, errors[] }
Файл: src/app/api/wms/warehouses/import-csv/route.ts
GET /api/wms/wave (нет описания)
GET /api/wms/wave — list waves (active по дефолту)
Файл: src/app/api/wms/wave/route.ts
POST /api/wms/wave (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/wave/route.ts
GET /api/wms/wave/[id] (нет описания)
GET /api/wms/wave/{id} — wave details with all supplies + pick progress per item.
Файл: src/app/api/wms/wave/[id]/route.ts
PATCH /api/wms/wave/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/wave/[id]/route.ts
POST /api/wms/wave/[id]/scan (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/wave/[id]/scan/route.ts
GET /api/wms/wb-sla оператора. Используется HomeHub countdown widget.
GET /api/wms/wb-sla — upcoming Wildberries / OZON deadlines для текущего
оператора. Используется HomeHub countdown widget.

Query:
  ?for_me=1   — фильтр по assignee_id = current_user
  ?within_h=24 — только deadlines в течение N часов (default: 24)

Returns: ближайший deadline + count overdue + count upcoming.
Файл: src/app/api/wms/wb-sla/route.ts
GET /api/wms/webhooks События (events array):
GET /api/wms/webhooks — список outgoing webhook'ов текущего ФФ.
POST — создать новый webhook.

События (events array):
  request.created · request.status_changed · request.completed
  invoice.issued · invoice.paid
  stock.low · stock.out
  return.received
Файл: src/app/api/wms/webhooks/route.ts
POST /api/wms/webhooks (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/webhooks/route.ts
GET /api/wms/webhooks/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/webhooks/[id]/route.ts
PATCH /api/wms/webhooks/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/webhooks/[id]/route.ts
DELETE /api/wms/webhooks/[id] (нет описания)

JSDoc отсутствует в исходнике.

Файл: src/app/api/wms/webhooks/[id]/route.ts
GET /api/wms/webhooks/[id]/deliveries История попыток доставки для этого webhook'а (последние N).
GET /api/wms/webhooks/[id]/deliveries?limit=50
История попыток доставки для этого webhook'а (последние N).
Файл: src/app/api/wms/webhooks/[id]/deliveries/route.ts
POST /api/wms/webhooks/[id]/test Отправляет тестовое событие на endpoint webhook'а с подписью HMAC.
POST /api/wms/webhooks/[id]/test

Отправляет тестовое событие на endpoint webhook'а с подписью HMAC.
Возвращает: { status, response, ms } — что вернул их сервер.

Не пишет в wms_webhook_delivery (это дебаг-проба, не реальная доставка).
Файл: src/app/api/wms/webhooks/[id]/test/route.ts
GET /api/wms/workflow/types (нет описания)
GET /api/wms/workflow/types — список типов заявок текущего ФФ
Файл: src/app/api/wms/workflow/types/route.ts
POST /api/wms/workflow/types (нет описания)
POST /api/wms/workflow/types — создать новый тип заявки
Файл: src/app/api/wms/workflow/types/route.ts
GET /api/wms/workflow/types/[id] (нет описания)
GET /api/wms/workflow/types/[id] — тип + список этапов
Файл: src/app/api/wms/workflow/types/[id]/route.ts
PATCH /api/wms/workflow/types/[id] (нет описания)
PATCH /api/wms/workflow/types/[id] — обновить + replace stages если переданы
Файл: src/app/api/wms/workflow/types/[id]/route.ts
DELETE /api/wms/workflow/types/[id] (нет описания)
DELETE /api/wms/workflow/types/[id]
Файл: src/app/api/wms/workflow/types/[id]/route.ts

Страницы дашборда

{{COUNT_PAGES}} страниц из src/app/(dashboard)/. Заголовок и описание берутся из <PageHeader title="..." description="..." />.

/acts (3)

МаршрутЗаголовокОписаниеФайл
/acts Acts src/app/(dashboard)/acts/page.tsx
/acts/[id] Acts src/app/(dashboard)/acts/[id]/page.tsx
/acts/new Новый акт Создание акта выполненных услуг src/app/(dashboard)/acts/new/page.tsx

/analytics (1)

МаршрутЗаголовокОписаниеФайл
/analytics Аналитика Расширенная аналитика продаж и клиентов src/app/(dashboard)/analytics/page.tsx

/bank-accounts (3)

МаршрутЗаголовокОписаниеФайл
/bank-accounts Bank Accounts src/app/(dashboard)/bank-accounts/page.tsx
/bank-accounts/[id] Bank Accounts src/app/(dashboard)/bank-accounts/[id]/page.tsx
/bank-accounts/new Новый банковский счёт Добавление расчётного счёта src/app/(dashboard)/bank-accounts/new/page.tsx

/billing (1)

МаршрутЗаголовокОписаниеФайл
/billing Биллинг Тарифный план и история платежей src/app/(dashboard)/billing/page.tsx

/calendar (1)

МаршрутЗаголовокОписаниеФайл
/calendar Calendar src/app/(dashboard)/calendar/page.tsx

/charts (1)

МаршрутЗаголовокОписаниеФайл
/charts Графики Витрина всех типов визуализаций src/app/(dashboard)/charts/page.tsx

/chat (1)

МаршрутЗаголовокОписаниеФайл
/chat Чат src/app/(dashboard)/chat/page.tsx

/completed (1)

МаршрутЗаголовокОписаниеФайл
/completed Completed src/app/(dashboard)/completed/page.tsx

/contracts (3)

МаршрутЗаголовокОписаниеФайл
/contracts Contracts src/app/(dashboard)/contracts/page.tsx
/contracts/[id] Contracts src/app/(dashboard)/contracts/[id]/page.tsx
/contracts/new Новый договор Добавление договора с контрагентом src/app/(dashboard)/contracts/new/page.tsx

/counterparties (3)

МаршрутЗаголовокОписаниеФайл
/counterparties Counterparties src/app/(dashboard)/counterparties/page.tsx
/counterparties/[id] Counterparties src/app/(dashboard)/counterparties/[id]/page.tsx
/counterparties/new Новый контрагент Добавление внешнего юрлица или ИП src/app/(dashboard)/counterparties/new/page.tsx

/crm (1)

МаршрутЗаголовокОписаниеФайл
/crm CRM Воронка продаж и активность менеджеров src/app/(dashboard)/crm/page.tsx

/customers (2)

МаршрутЗаголовокОписаниеФайл
/customers Клиенты База клиентов и история заказов src/app/(dashboard)/customers/page.tsx
/customers/[id] Customers src/app/(dashboard)/customers/[id]/page.tsx

/dashboard (1)

МаршрутЗаголовокОписаниеФайл
/dashboard Dashboard src/app/(dashboard)/page.tsx

/databases (3)

МаршрутЗаголовокОписаниеФайл
/databases Databases src/app/(dashboard)/databases/page.tsx
/databases/[id] Databases src/app/(dashboard)/databases/[id]/page.tsx
/databases/new Новая база данных Регистрация 1С-базы src/app/(dashboard)/databases/new/page.tsx

/design-system (1)

МаршрутЗаголовокОписаниеФайл
/design-system ООО «Альфа Логистик» Контрагент · ИНН 7701234567 · Москва src/app/(dashboard)/design-system/page.tsx

/files (1)

МаршрутЗаголовокОписаниеФайл
/files Файлы src/app/(dashboard)/files/page.tsx

/guide (2)

МаршрутЗаголовокОписаниеФайл
/guide Справочники Товары, контрагенты и другие справочники src/app/(dashboard)/guide/page.tsx
/guide/[id] Guide src/app/(dashboard)/guide/[id]/page.tsx

/inbox (1)

МаршрутЗаголовокОписаниеФайл
/inbox Inbox src/app/(dashboard)/inbox/page.tsx

/invoices (3)

МаршрутЗаголовокОписаниеФайл
/invoices Invoices src/app/(dashboard)/invoices/page.tsx
/invoices/[id] Invoices src/app/(dashboard)/invoices/[id]/page.tsx
/invoices/new Новый счёт Создание счёта на оплату src/app/(dashboard)/invoices/new/page.tsx

/kanban (1)

МаршрутЗаголовокОписаниеФайл
/kanban Канбан Доска задач с drag & drop src/app/(dashboard)/kanban/page.tsx

/label-template (1)

МаршрутЗаголовокОписаниеФайл
/label-template Label Template ${PRODUCT.brand} · ${PRODUCT.article} src/app/(dashboard)/label-template/page.tsx

/mail (1)

МаршрутЗаголовокОписаниеФайл
/mail Почта src/app/(dashboard)/mail/page.tsx

/notifications (1)

МаршрутЗаголовокОписаниеФайл
/notifications Уведомления Непрочитанных: ${unreadCount} src/app/(dashboard)/notifications/page.tsx

/organizations (3)

МаршрутЗаголовокОписаниеФайл
/organizations Organizations src/app/(dashboard)/organizations/page.tsx
/organizations/[id] Organizations src/app/(dashboard)/organizations/[id]/page.tsx
/organizations/new Новая организация Добавление нашего юридического лица src/app/(dashboard)/organizations/new/page.tsx

/overdue (1)

МаршрутЗаголовокОписаниеФайл
/overdue Overdue src/app/(dashboard)/overdue/page.tsx

/projects (3)

МаршрутЗаголовокОписаниеФайл
/projects Projects src/app/(dashboard)/projects/page.tsx
/projects/[id] Projects src/app/(dashboard)/projects/[id]/page.tsx
/projects/new Новый проект Создание проекта src/app/(dashboard)/projects/new/page.tsx

/saas (1)

МаршрутЗаголовокОписаниеФайл
/saas SaaS Метрики подписочного бизнеса src/app/(dashboard)/saas/page.tsx

/seller (2)

МаршрутЗаголовокОписаниеФайл
/seller/fbo Fbo src/app/(dashboard)/seller/fbo/page.tsx
/seller/fbo/new New src/app/(dashboard)/seller/fbo/new/page.tsx

/settings (2)

МаршрутЗаголовокОписаниеФайл
/settings Настройки src/app/(dashboard)/settings/page.tsx
/settings/team Команда Приглашения юзеров в текущую компанию по email-токену (валидно 7 дней) src/app/(dashboard)/settings/team/page.tsx

/subscriptions (2)

МаршрутЗаголовокОписаниеФайл
/subscriptions Subscriptions src/app/(dashboard)/subscriptions/page.tsx
/subscriptions/[id] Subscriptions src/app/(dashboard)/subscriptions/[id]/page.tsx

/support (1)

МаршрутЗаголовокОписаниеФайл
/support Помощь Часто задаваемые вопросы и поддержка src/app/(dashboard)/support/page.tsx

/tariffs (3)

МаршрутЗаголовокОписаниеФайл
/tariffs Tariffs src/app/(dashboard)/tariffs/page.tsx
/tariffs/[id] Tariffs src/app/(dashboard)/tariffs/[id]/page.tsx
/tariffs/new Новый тариф Создание тарифа src/app/(dashboard)/tariffs/new/page.tsx

/tasks (3)

МаршрутЗаголовокОписаниеФайл
/tasks Tasks src/app/(dashboard)/tasks/page.tsx
/tasks/[id] Tasks src/app/(dashboard)/tasks/[id]/page.tsx
/tasks/new Новая задача Создание задачи src/app/(dashboard)/tasks/new/page.tsx

/today (1)

МаршрутЗаголовокОписаниеФайл
/today Today src/app/(dashboard)/today/page.tsx

/today-v2 (1)

МаршрутЗаголовокОписаниеФайл
/today-v2 Today V2 src/app/(dashboard)/today-v2/page.tsx

/todo-test (1)

МаршрутЗаголовокОписаниеФайл
/todo-test Todo API — smoke-test Проверка связки BCP → Next-proxy → todo (http://apiinpacking.ru/todo/hs/v1/). Токен читается из localStorage (bcp-token). src/app/(dashboard)/todo-test/page.tsx

/upcoming (1)

МаршрутЗаголовокОписаниеФайл
/upcoming Upcoming src/app/(dashboard)/upcoming/page.tsx

/users (3)

МаршрутЗаголовокОписаниеФайл
/users Users src/app/(dashboard)/users/page.tsx
/users/[id] Users src/app/(dashboard)/users/[id]/page.tsx
/users/new Новый пользователь Заполните данные и сохраните src/app/(dashboard)/users/new/page.tsx

/wms (119)

МаршрутЗаголовокОписаниеФайл
/wms WMS — управление складом Адресное хранение, журнал движений, мульти-маркетплейс FBS src/app/(dashboard)/wms/page.tsx
/wms/account/messengers Мессенджеры Привяжите Telegram и MAX к своему аккаунту, чтобы получать уведомления прямо в мессенджере. src/app/(dashboard)/wms/account/messengers/page.tsx
/wms/admin Управление платформой Все системные tools — мониторинг, логи, журнал, здоровье src/app/(dashboard)/wms/admin/page.tsx
/wms/admin/audit Журнал изменений События по заявкам · ${items.length} записей src/app/(dashboard)/wms/admin/audit/page.tsx
/wms/admin/cron Cron-monitor Все периодические задачи и их последние запуски. Авто-обновление 60 сек. src/app/(dashboard)/wms/admin/cron/page.tsx
/wms/admin/health Здоровье системы Cron'ы, статистика БД, активные подписки. Обновляется по требованию. src/app/(dashboard)/wms/admin/health/page.tsx
/wms/admin/notifications Журнал уведомлений Telegram + email notifications за последние 7 дней src/app/(dashboard)/wms/admin/notifications/page.tsx
/wms/analytics Аналитика ФФ Top-клиенты, должники, оборачиваемость, загрузка склада — за 30 дней src/app/(dashboard)/wms/analytics/page.tsx
/wms/audit Audit src/app/(dashboard)/wms/audit/page.tsx
/wms/audit/search Audit log — расширенный поиск src/app/(dashboard)/wms/audit/search/page.tsx
/wms/boxes Коробки Учёт коробок, сканирование, перемещения. Всего: ${total} src/app/(dashboard)/wms/boxes/page.tsx
/wms/boxes/[id] Boxes src/app/(dashboard)/wms/boxes/[id]/page.tsx
/wms/bundles Комплектация Товары-комплекты: композиции из нескольких товаров. При отгрузке списываются компоненты. src/app/(dashboard)/wms/bundles/page.tsx
/wms/bundles/[id] Bundles Состав комплекта и стоимость сборки. При отгрузке компоненты списываются автоматически. src/app/(dashboard)/wms/bundles/[id]/page.tsx
/wms/cabinet Cabinet src/app/(dashboard)/wms/cabinet/page.tsx
/wms/cabinet/changelog История изменений Все события касающиеся ваших товаров, заявок, счетов и FBO поставок src/app/(dashboard)/wms/cabinet/changelog/page.tsx
/wms/cabinet/finance Финансы Счета и баланс · ${invoices.length} счёт(ов) всего src/app/(dashboard)/wms/cabinet/finance/page.tsx
/wms/cabinet/news Что нового на платформе Обновления, новые фичи и важные объявления src/app/(dashboard)/wms/cabinet/news/page.tsx
/wms/cabinet/products Мои товары Каталог: ${total} SKU src/app/(dashboard)/wms/cabinet/products/page.tsx
/wms/cabinet/requests Мои заявки Всего: ${items.length} src/app/(dashboard)/wms/cabinet/requests/page.tsx
/wms/cabinet/stocks Остатки SKU с движениями: ${items.length} · Всего единиц: ${totalUnits} src/app/(dashboard)/wms/cabinet/stocks/page.tsx
/wms/cabinet/storage Хранение Сколько вы платите за хранение товаров на складе src/app/(dashboard)/wms/cabinet/storage/page.tsx
/wms/calculator Калькулятор услуг Быстрый расчёт стоимости для предложения клиенту. Цены с учётом override-тарифа. src/app/(dashboard)/wms/calculator/page.tsx
/wms/cells Ячейки src/app/(dashboard)/wms/cells/page.tsx
/wms/cells/[id] Cells src/app/(dashboard)/wms/cells/[id]/page.tsx
/wms/clients Клиенты Селлеры, чьи товары лежат на складе. Multi-tenant: свой остаток + тариф + баланс. src/app/(dashboard)/wms/clients/page.tsx
/wms/clients/[id] Clients Создан: ${new Date(client.created_at).toLocaleString('ru-RU')} src/app/(dashboard)/wms/clients/[id]/page.tsx
/wms/clients/invites Приглашения клиентов-селлеров Список ссылок-приглашений отправленных селлерам для подключения к этому ФФ. src/app/(dashboard)/wms/clients/invites/page.tsx
/wms/dbs Dbs src/app/(dashboard)/wms/dbs/page.tsx
/wms/dbs/[id] Dbs src/app/(dashboard)/wms/dbs/[id]/page.tsx
/wms/dbs/work Work src/app/(dashboard)/wms/dbs/work/page.tsx
/wms/departments Отделы Иерархическая структура отделов фулфилмент-центра src/app/(dashboard)/wms/departments/page.tsx
/wms/docs/api Api src/app/(dashboard)/wms/docs/api/page.tsx
/wms/fbo FBO Поставки партий на склад маркетплейса · ${totalAll} всего, ${supplies.length} в выборке src/app/(dashboard)/wms/fbo/page.tsx
/wms/fbo/[id] Fbo src/app/(dashboard)/wms/fbo/[id]/page.tsx
/wms/fbo/[id]/pack Упаковка FBO ${supply.number} src/app/(dashboard)/wms/fbo/[id]/pack/page.tsx
/wms/fbo/metrics Metrics src/app/(dashboard)/wms/fbo/metrics/page.tsx
/wms/fbo/new Новая FBO-поставка Отгрузка на склад маркетплейса. Wizard 4 шага. src/app/(dashboard)/wms/fbo/new/page.tsx
/wms/fbo/work Work src/app/(dashboard)/wms/fbo/work/page.tsx
/wms/fbs Fbs src/app/(dashboard)/wms/fbs/page.tsx
/wms/fbs/[id] Fbs src/app/(dashboard)/wms/fbs/[id]/page.tsx
/wms/fbs/work Work src/app/(dashboard)/wms/fbs/work/page.tsx
/wms/finance Финансы Биллинг ФФ — услуги, счета, оплаты src/app/(dashboard)/wms/finance/page.tsx
/wms/finance/calculator Калькулятор услуг Quick-quote инструмент для коммерческого предложения. Можно сохранить как черновик счёта. src/app/(dashboard)/wms/finance/calculator/page.tsx
/wms/finance/debtors Должники src/app/(dashboard)/wms/finance/debtors/page.tsx
/wms/finance/invoices Счета Всего ${items.length} на сумму ${totalSum.toLocaleString('ru-RU')} ₽ src/app/(dashboard)/wms/finance/invoices/page.tsx
/wms/finance/invoices/[id] Invoices src/app/(dashboard)/wms/finance/invoices/[id]/page.tsx
/wms/finance/invoices/new Новый счёт Ручное создание (manual) — для quick-quote или внеплановых работ src/app/(dashboard)/wms/finance/invoices/new/page.tsx
/wms/finance/reconciliation Акт сверки Сальдо расчётов с клиентом за период src/app/(dashboard)/wms/finance/reconciliation/page.tsx
/wms/finance/services Каталог услуг ${items.length} позиций. Это default-цены ФФ — переопределяются per-client тарифом. src/app/(dashboard)/wms/finance/services/page.tsx
/wms/finance/storage Биллинг хранения Помесячная генерация счетов за хранение остатков (m³ × дни × тариф) src/app/(dashboard)/wms/finance/storage/page.tsx
/wms/finance/storage-log Журнал хранения Ежедневное начисление за хранение · Период ${from} — ${to} · Сумма ${totalCharged.toLocaleString('ru-RU')} ₽ src/app/(dashboard)/wms/finance/storage-log/page.tsx
/wms/finance/tariffs Импорт тарифов из CSV Массовая загрузка клиентских тарифов на услуги src/app/(dashboard)/wms/finance/tariffs/page.tsx
/wms/inbound Inbound src/app/(dashboard)/wms/inbound/page.tsx
/wms/inbound/[id] Inbound src/app/(dashboard)/wms/inbound/[id]/page.tsx
/wms/inbound/[id]/work Приёмка ${data.number} ${data.client_name ?? '—'} · ${data.warehouse_name ?? 'без склада'} src/app/(dashboard)/wms/inbound/[id]/work/page.tsx
/wms/integrations Интеграции с маркетплейсами Статус подключений WB / Ozon / Yandex Market и cron-задач синхронизации src/app/(dashboard)/wms/integrations/page.tsx
/wms/inventory Инвентаризации Сканирование склада → сравнение с учётной системой → adjust-движения по расхождениям src/app/(dashboard)/wms/inventory/page.tsx
/wms/inventory/[id]/work Инвентаризация ${data.number} ${data.client_name ?? '—'} · ${data.warehouse_name ?? '—'} src/app/(dashboard)/wms/inventory/[id]/work/page.tsx
/wms/kiz КИЗ — Честный Знак Всего: ${items.length} src/app/(dashboard)/wms/kiz/page.tsx
/wms/kiz/[id] ${kiz.gtin}-${kiz.serial} Создан ${new Date(kiz.created_at).toLocaleString('ru-RU')} src/app/(dashboard)/wms/kiz/[id]/page.tsx
/wms/kiz/reprint Перепечать КИЗ по сканеру Отсканируйте DataMatrix с этикетки — система найдёт КИЗ и сгенерирует новую этикетку src/app/(dashboard)/wms/kiz/reprint/page.tsx
/wms/labels Шаблоны этикеток Создавайте шаблоны под разные товары, клиентов и размеры. При печати поля подставляются из карточки товара. src/app/(dashboard)/wms/labels/page.tsx
/wms/labels/[id] Labels src/app/(dashboard)/wms/labels/[id]/page.tsx
/wms/labels/templates Шаблоны этикеток Дизайнер v2 — drag-drop, мульти-формат (ZPL/TSPL/EPL/PDF) с поддержкой Zebra/TSC/Xprinter src/app/(dashboard)/wms/labels/templates/page.tsx
/wms/labels/templates/[id]/edit Edit src/app/(dashboard)/wms/labels/templates/[id]/edit/page.tsx
/wms/labels/templates/new Новый шаблон src/app/(dashboard)/wms/labels/templates/new/page.tsx
/wms/lots Партии и сроки годности FEFO-сортировка: самые скорые к истечению — сверху. Для производственных, FMCG и химических товаров. src/app/(dashboard)/wms/lots/page.tsx
/wms/manifests ТТН (товарно-транспортные накладные) Объединение нескольких заявок в одну отгрузку — для водителя или маркетплейс-курьера src/app/(dashboard)/wms/manifests/page.tsx
/wms/manifests/[id] ТТН ${data.number} src/app/(dashboard)/wms/manifests/[id]/page.tsx
/wms/manifests/new Новая ТТН Выберите заявки и заполните контакт водителя src/app/(dashboard)/wms/manifests/new/page.tsx
/wms/movements Журнал движений История всех операций со складом (event-log). Из этого журнала производятся остатки. src/app/(dashboard)/wms/movements/page.tsx
/wms/movements/transfer Перемещение между ячейками Прямое перемещение без оформления заявки. Movement type='transfer'. src/app/(dashboard)/wms/movements/transfer/page.tsx
/wms/news Что нового Обновления платформы, фичи, инциденты src/app/(dashboard)/wms/news/page.tsx
/wms/onboarding Добро пожаловать в BCP Загружаем настройку... src/app/(dashboard)/wms/onboarding/page.tsx
/wms/onboarding/bulk-import Onboarding нового клиента Загрузите данные нового ФФ в 3 шага. Цель — поднять рабочее окружение за 30 минут. src/app/(dashboard)/wms/onboarding/bulk-import/page.tsx
/wms/pricing Тарифы Выберите подходящий тариф. Можно сменить в любое время — оплата pro-rated. src/app/(dashboard)/wms/pricing/page.tsx
/wms/print/act/[id] Act src/app/(dashboard)/wms/print/act/[id]/page.tsx
/wms/print/box-bulk Box Bulk src/app/(dashboard)/wms/print/box-bulk/page.tsx
/wms/print/box/[id] Box src/app/(dashboard)/wms/print/box/[id]/page.tsx
/wms/print/cells Cells src/app/(dashboard)/wms/print/cells/page.tsx
/wms/print/fbo-sscc/[id] Fbo Sscc src/app/(dashboard)/wms/print/fbo-sscc/[id]/page.tsx
/wms/print/fbo/[id] Fbo src/app/(dashboard)/wms/print/fbo/[id]/page.tsx
/wms/print/fbs-bulk Fbs Bulk src/app/(dashboard)/wms/print/fbs-bulk/page.tsx
/wms/print/fbs/[id] Fbs src/app/(dashboard)/wms/print/fbs/[id]/page.tsx
/wms/print/invoice/[id] Invoice src/app/(dashboard)/wms/print/invoice/[id]/page.tsx
/wms/print/kiz Kiz src/app/(dashboard)/wms/print/kiz/page.tsx
/wms/print/picklist-bulk Picklist Bulk src/app/(dashboard)/wms/print/picklist-bulk/page.tsx
/wms/print/picklist/[id] Picklist src/app/(dashboard)/wms/print/picklist/[id]/page.tsx
/wms/print/reconciliation/[client_id] Reconciliation src/app/(dashboard)/wms/print/reconciliation/[client_id]/page.tsx
/wms/print/sheet/[id] Sheet src/app/(dashboard)/wms/print/sheet/[id]/page.tsx
/wms/print/template/[id] Template src/app/(dashboard)/wms/print/template/[id]/page.tsx
/wms/products Товары src/app/(dashboard)/wms/products/page.tsx
/wms/products/[id] Products Клиент: ${product.client_name} · Создан ${new Date(product.created_at).toLocaleString('ru-RU')} src/app/(dashboard)/wms/products/[id]/page.tsx
/wms/pvz Пункты выдачи (ПВЗ) Активных: ${activeCount} · Всего: ${items.length} src/app/(dashboard)/wms/pvz/page.tsx
/wms/reports Отчёты Аналитика за последние ${days} дн. src/app/(dashboard)/wms/reports/page.tsx
/wms/reports/export Экспорт отчётов Скачать Excel/CSV для бухгалтерии src/app/(dashboard)/wms/reports/export/page.tsx
/wms/requests Заявки src/app/(dashboard)/wms/requests/page.tsx
/wms/returns Возвраты ${total} возвратов от покупателей через MP src/app/(dashboard)/wms/returns/page.tsx
/wms/returns/[id] Возврат ${data.external_id} ${data.marketplace.toUpperCase()} · ${data.client_name ?? '—'} · ${data.product_sku ?? '—'} src/app/(dashboard)/wms/returns/[id]/page.tsx
/wms/settings Настройки фулфилмент-центра Глобальные параметры ФФ. Настройки клиентов (MP-ключи, тарифы) — в карточках клиентов. src/app/(dashboard)/wms/settings/page.tsx
/wms/settings/company Реквизиты компании Юридические реквизиты ФФ как контрагента платформы. Используются для счетов, актов, договоров. Не путать с реквизитами ваших клиентов-селлеров. src/app/(dashboard)/wms/settings/company/page.tsx
/wms/settings/max MAX-бот Свой MAX-бот для уведомлений вашим клиентам и сотрудникам. White-label — клиенты видят имя вашего бота, не платформенного. src/app/(dashboard)/wms/settings/max/page.tsx
/wms/settings/platform Настройки платформы Реквизиты ИП Гусейнов / BCP, brand, дефолты биллинга. Используются для автогенерации счетов клиентам-ФФ за подписку. Singleton — одна запись на всю платформу. src/app/(dashboard)/wms/settings/platform/page.tsx
/wms/settings/security Безопасность Двухфакторная аутентификация (2FA) через Authenticator (Google / Yandex / Microsoft) src/app/(dashboard)/wms/settings/security/page.tsx
/wms/settings/telegram Telegram-бот Свой Telegram-бот для уведомлений вашим клиентам и сотрудникам. White-label — клиенты видят имя вашего бота, не платформенного. src/app/(dashboard)/wms/settings/telegram/page.tsx
/wms/settings/webhooks Webhooks ${items.length} настроенных · ${items.filter((i) => i.is_active).length} активных src/app/(dashboard)/wms/settings/webhooks/page.tsx
/wms/settings/workflow Типы заявок Workflow-шаблоны: типы заявок с конфигурируемыми этапами и SLA-таймерами src/app/(dashboard)/wms/settings/workflow/page.tsx
/wms/settings/workflow/[id] Workflow src/app/(dashboard)/wms/settings/workflow/[id]/page.tsx
/wms/stillages/[id] Стеллаж ${stillage.code} src/app/(dashboard)/wms/stillages/[id]/page.tsx
/wms/stocks Остатки SKU: ${total} · Доступно: ${items.reduce((s, x) => s + Math.max(0, x.qty_available), 0)} · Резерв: ${items.reduce((s, x) => s + x.qty_reserved, 0)} · Брак: ${items.reduce((s, x) => s + x.qty_defective, 0)} src/app/(dashboard)/wms/stocks/page.tsx
/wms/users Сотрудники Сотрудники склада с ролями. 8 системных ролей + индивидуальные права. src/app/(dashboard)/wms/users/page.tsx
/wms/users/[id] Users src/app/(dashboard)/wms/users/[id]/page.tsx
/wms/users/payroll KPI сотрудников ФФ ${usersActive} активных из ${usersTotal} · период ${period.from} → ${period.to} src/app/(dashboard)/wms/users/payroll/page.tsx
/wms/warehouses Склады Адресное хранение: склад → стеллаж → этаж → ячейка src/app/(dashboard)/wms/warehouses/page.tsx
/wms/warehouses/[id] Warehouses Создан: ${new Date(warehouse.created_at).toLocaleString('ru-RU')} src/app/(dashboard)/wms/warehouses/[id]/page.tsx
/wms/warehouses/[id]/map Карта склада: ${warehouse.name} ${(W / 100).toFixed(1)}×${(H / 100).toFixed(1)} м · сетка ${grid} см · ${stillages.length} стеллажей · ${decorations.length} декораций src/app/(dashboard)/wms/warehouses/[id]/map/page.tsx
/wms/waves Waves src/app/(dashboard)/wms/waves/page.tsx
/wms/waves/[id] Waves src/app/(dashboard)/wms/waves/[id]/page.tsx

Architecture Decision Records

Список ADR из Obsidian-vault'а (knowledge/05-Decisions/). Ссылки локальные — открываются на C:\vibe-dev\.