Обзор проекта
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 между инстансами |
| Доступ изнутри LAN | http://10.0.0.140:7000 |
| Доступ снаружи | http://89.23.35.71:7000 — только если на роутере добавлен NAT-форвард :7000 → 10.0.0.140:7000 |
| Источник API | apiinpacking.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.
Распределение ресурсов
| Сервис | RAM | CPU |
|---|---|---|
| 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). Walkssrc/app/api/**/route.tsand extracts JSDoc above eachexport async function GET/POST/...for method + description. Walkssrc/app/(dashboard)/**/page.tsxand pulls<PageHeader title="" description="">. ReadsCHANGELOG.md([Unreleased] + last 3 versioned/date sections, body truncated at 8 KB),README.md(rendered with a minimal GFM-compatible markdown renderer including tables), andknowledge/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 viaprefers-color-scheme). Theme toggle persists tolocalStorage. - 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:build→tsx ops/generate-docs.tsdocs:serve→npx serve docs(optional local preview)
Serving on prod — added src/app/docs/route.ts (Next.js route handler, nodejs runtime, force-static):
GET /docs→ readsdocs/index.htmlfrom disk on cold start, caches in module scope keyed by mtime (re-reads only whendocs:buildupdates the file). Returns 200 withContent-Type: text/html; charset=utf-8+Cache-Control: public, max-age=300.- If
docs/index.htmlis 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 dev → curl 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.ts—apiError()helper: resolves locale, loads translation, returns localized NextResponse. Also exportsinterpolateMessage()andgetLocalizedMessage().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— addedgetRequestLocale(req): Accept-Language header → user preferredlocale → tenant defaultlocale → 'ru'.public/locales/{ru,en,uz,kz}/common.json— addederrors.*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.tssrc/app/api/wms/fbo/supplies/[id]/propose-date/route.tssrc/app/api/wms/fbo/supplies/[id]/respond-proposal/route.tssrc/app/api/wms/fbo/supplies/[id]/complete/route.tssrc/app/api/wms/fbo/supplies/[id]/bind-wb/route.tssrc/app/api/wms/returns/[id]/classify/route.tssrc/app/api/wms/returns/[id]/unclassify/route.tssrc/app/api/wms/fbo/returns/from-supply/[supplyId]/initiate/route.tssrc/app/api/wms/tsd/fbo/scan-bind/route.tssrc/app/api/wms/account/preferred-locale/route.ts
Modified — tests (updated rbac mocks: requireRole → actorHasRole for routes that migrated):
tests-unit/api/wms/storage-billing.test.tstests-unit/api/wms/fbo/propose-date.test.tstests-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-remindercron silence >7h,cleanup-expired-tokenscron 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 bytenant_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 existingforwardToLocalBridge, 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).
Как пополнять
- После каждого deploy — добавить запись в
[Unreleased]под нужной секцией (Added/Changed/Fixed/Removed). - Если изменение архитектурное — параллельно создать ADR в
vibe-dev/knowledge/05-Decisions/YYYY-MM-DD-name.mdи сослаться отсюда. - При git tag/release — переместить
[Unreleased]в новый раздел## [vX.Y.Z] — YYYY-MM-DDи оставить пустой[Unreleased]сверху. - Помощник:
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`).
POST /api/auth/forgot-password body: { email }
POST /api/auth/forgot-password
body: { email }
Идемпотентно: всегда возвращает 200 (не палим существование email).
Если email есть и valid → создаём reset_token и шлём ссылку.
GET /api/auth/login (нет описания)
JSDoc отсутствует в исходнике.
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).
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.
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).
GET /api/auth/seller/invites Возвращает active (pending) приглашения для email текущего залогиненного
GET /api/auth/seller/invites Возвращает active (pending) приглашения для email текущего залогиненного селлера. Используется в /seller dashboard inbox.
GET /api/auth/seller/me Возвращает профиль текущего залогиненного селлера + список его memberships
GET /api/auth/seller/me Возвращает профиль текущего залогиненного селлера + список его memberships (active wms_client rows в разных ФФ). Если не залогинен — 401.
GET /api/auth/seller/notifications Возвращает список known event_types + текущие prefs пользователя
GET /api/auth/seller/notifications Возвращает список known event_types + текущие prefs пользователя (default = true для отсутствующих rows).
PATCH /api/auth/seller/notifications body: { event_type, enabled }
PATCH /api/auth/seller/notifications
body: { event_type, enabled }
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()
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 всё равно обязательны.
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. Юзер должен быть залогинен.
DELETE /api/auth/seller/telegram/bind (нет описания)
DELETE /api/auth/seller/telegram/bind — отвязать Telegram
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.
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)
/api/cron (6)
GET /api/cron/ai-health-check (нет описания)
JSDoc отсутствует в исходнике.
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 }
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 }
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[] }
GET /api/cron/storage-billing (нет описания)
JSDoc отсутствует в исходнике.
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).
/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.
/api/platform (11)
POST /api/platform/billing/webhook/stripe (нет описания)
JSDoc отсутствует в исходнике.
GET /api/platform/invites (нет описания)
GET /api/platform/invites — список приглашений в текущий tenant
POST /api/platform/invites Body: { email, role? }
POST /api/platform/invites
Body: { email, role? }
Создаёт invite-token, валиден 7 дней. URL = /invite/{token}
GET /api/platform/invites/[token]/accept (нет описания)
GET /api/platform/invites/[token]/accept — детали инвайта (для UI accept-страницы)
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
GET /api/platform/settings (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/platform/settings (нет описания)
JSDoc отсутствует в исходнике.
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.
GET /api/platform/tenants Возвращает список доступных компаний для текущего юзера:
GET /api/platform/tenants Возвращает список доступных компаний для текущего юзера: - Для платформенного owner — все wms_ff - Для обычного юзера — только те где у него есть membership - Маркер is_current — какой сейчас активный (из active_tenant cookie)
POST /api/platform/tenants Body: { tenant_id }
POST /api/platform/tenants/switch
Body: { tenant_id }
Устанавливает cookie active_tenant. Юзер должен быть platform_owner
либо иметь membership к этому tenant'у.
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.
/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.
/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).
DELETE /api/tsd/auth/session Headers: X-TSD-Token: ...
DELETE /api/tsd/auth/session Headers: X-TSD-Token: ... Завершает сессию TSD.
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 → корректировка.
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 при сканировании.
/api/webhooks (1)
POST /api/webhooks/max/[ff_id] (нет описания)
JSDoc отсутствует в исходнике.
/api/wms (368)
GET /api/wms/account/link-max (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/account/link-max (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/account/link-max (нет описания)
JSDoc отсутствует в исходнике.
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.
PATCH /api/wms/account/preferred-locale (нет описания)
JSDoc отсутствует в исходнике.
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 последних событий по фильтру.
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).
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.
GET /api/wms/admin/health Сводка здоровья системы для ФФ-админа:
GET /api/wms/admin/health Сводка здоровья системы для ФФ-админа: - Статус БД и количество строк по ключевым таблицам - Последние записи журнала хранения (когда последний раз cron работал) - Последние записи WB-sync (если table'ы существуют) - Последние backups (по размеру log) - Активные cron'ы (мы знаем какие должны быть, проверяем по последним меткам) Доступно только platform_owner.
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 }
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.
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.
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.
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).
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 одной рукой).
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.
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 поставщику видишь когда
критично, сколько заказать.
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).
POST /api/wms/ai/requests/[id]/summarize body: { language?: 'ru'|'en' }
POST /api/wms/ai/requests/[id]/summarize
body: { language?: 'ru'|'en' }
AI-резюме заявки: что произошло, текущее состояние, риски, рекомендации.
Для клиента (понятным языком) или для оператора (с действиями).
GET /api/wms/ai/status Возвращает состояние AI-провайдеров (какие env настроены) + usage stats
GET /api/wms/ai/status Возвращает состояние AI-провайдеров (какие env настроены) + usage stats за последние 7 дней. Полезно для admin/debug. Маскирует ключи — отображает только presence (true/false), не значения.
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).
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 расширим.
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).
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().
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' }.
POST /api/wms/app/register (нет описания)
JSDoc отсутствует в исходнике.
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.
POST /api/wms/auth/2fa/disable body: { password }
POST /api/wms/auth/2fa/disable
body: { password }
Отключает 2FA. Требует подтверждения паролем чтобы атакующий с украденной
сессией не мог отключить 2FA.
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-коды показываются один раз.
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 }
GET /api/wms/auth/2fa/status Возвращает: { enabled, enabled_at, backup_remaining }
GET /api/wms/auth/2fa/status
Возвращает: { enabled, enabled_at, backup_remaining }
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).
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 — для мониторинга активности.
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 не применяется — ТСД физически в руках работника
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).
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.
GET /api/wms/billing/reconciliation Акт сверки за период:
GET /api/wms/billing/reconciliation?client_id=&from=&to= Акт сверки за период: - Все invoices клиента созданные в период - Все payments клиента за период - Балансы: открытые / оплаченные / разница Если to не задан — конец текущего месяца. Если from — начало.
GET /api/wms/billing/storage (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/billing/storage (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/boxes (нет описания)
GET /api/wms/boxes?search=&barcode=&client_id=&page=&per_page=
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.
GET /api/wms/boxes/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/boxes/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/boxes/[id] (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/boxes/[id]/items body: { product_id, qty } — добавить/обновить товар в коробке
POST /api/wms/boxes/[id]/items
body: { product_id, qty } — добавить/обновить товар в коробке
Идемпотентно (UPSERT по box+product). Для увеличения количества вызывайте повторно.
DELETE /api/wms/boxes/[id]/items (нет описания)
DELETE /api/wms/boxes/[id]/items?item_id=X — удалить позицию из коробки
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, если коробок там больше нет)
GET /api/wms/bundles Список товаров-комплектов (is_bundle=true) с количеством компонент и
GET /api/wms/bundles Список товаров-комплектов (is_bundle=true) с количеством компонент и прогнозом сколько комплектов мы можем собрать прямо сейчас.
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 и устанавливает компоненты атомарно.
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.
DELETE /api/wms/bundles/[id] Снимает флаг is_bundle и удаляет компоненты (товар остаётся).
DELETE /api/wms/bundles/[id] Снимает флаг is_bundle и удаляет компоненты (товар остаётся).
GET /api/wms/cabinet/changelog (нет описания)
JSDoc отсутствует в исходнике.
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 период: текущий месяц.
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
чтобы видеть какие услуги пересчитаны под клиента.
GET /api/wms/cells Список ячеек с фильтрацией.
GET /api/wms/cells?warehouse_id=&stillage=&address=&status=&type= Список ячеек с фильтрацией. Используется: - /wms/warehouses/[id] — список ячеек склада - Сценарий «scan cell» — найти ячейку по address для перемещения коробки
GET /api/wms/cells/[id] (нет описания)
GET /api/wms/cells/[id] — детали ячейки + список коробок в ней.
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.
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».
GET /api/wms/clients (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/clients (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/clients/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/clients/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/clients/[id] (нет описания)
JSDoc отсутствует в исходнике.
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 ФФ.
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 за один раз.
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.
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[] }
POST /api/wms/clients/import (нет описания)
JSDoc отсутствует в исходнике.
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р может ещё не быть залогинен).
DELETE /api/wms/clients/invite/[token] Revoke — только для invited_by из этого ФФ.
DELETE /api/wms/clients/invite/[token] Revoke — только для invited_by из этого ФФ.
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р сразу попадает в кабинет этого ФФ)
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 ФФ (не клиенты-селлеры).
GET /api/wms/clients/invites Возвращает список invite'ов в текущем ФФ.
GET /api/wms/clients/invites?status=pending|accepted|revoked|expired|all Возвращает список invite'ов в текущем ФФ. Для UI каталога приглашений (/wms/clients/invites).
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
POST /api/wms/courier (нет описания)
JSDoc отсутствует в исходнике.
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, ... }] }
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.
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).
GET /api/wms/departments (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/departments (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/departments/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/departments/[id] (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/device/register (нет описания)
JSDoc отсутствует в исходнике.
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».
GET /api/wms/events (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/export/returns (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/export/storage-events (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/export/supplies (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/metrics (нет описания)
JSDoc отсутствует в исходнике.
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).
GET /api/wms/fbo/supplies (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/fbo/supplies (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/fbo/supplies/[id]/bind-wb (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/fbo/supplies/[id]/box-cargo-bind (нет описания)
JSDoc отсутствует в исходнике.
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).
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].
POST /api/wms/fbo/supplies/[id]/propose-date (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/fbo/supplies/[id]/qr-upload (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/fbo/supplies/[id]/respond-proposal (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbo/supplies/[id]/sscc (нет описания)
JSDoc отсутствует в исходнике.
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.
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 если приёмка завершена.
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'.
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.
POST /api/wms/fbo/supplies/seller-create (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/fbs/pack-photo Список фото по заявке.
GET /api/wms/fbs/pack-photo?request_id=... Список фото по заявке.
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.
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).
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).
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» в архиве.
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.
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? }
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: детальный список операций
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
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: уникальные клиенты в выборке
POST /api/wms/finance/storage-log/run Ручной триггер дневного начисления хранения. Используется UI-кнопкой
POST /api/wms/finance/storage-log/run Ручной триггер дневного начисления хранения. Используется UI-кнопкой «Начислить сейчас» в /wms/finance/storage-log. Логика идентична cron, но scope только текущий ФФ (через getTenantId) — без secret'а. Идемпотентно: повторный запуск в тот же день не дублирует записи.
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: [...] }
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?: {...} }
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 } }
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-подключённых клиентов.
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-подключённых клиентов.
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
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.
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.
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).
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'ом.
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: [] }
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'.
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.
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) поставки, для печати на стикере.
POST /api/wms/integrations/wb/sync (нет описания)
JSDoc отсутствует в исходнике.
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.
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.
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-ключом.
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`.
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.
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-инсайты что именно пошло не так и как исправить процессы.
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, чтобы оператор
увидел общую сумму расхождений до подтверждения.
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 } }
GET /api/wms/invoices (нет описания)
GET /api/wms/invoices?client_id=&status=&source_type=
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?
}
GET /api/wms/invoices/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/invoices/[id] (нет описания)
PATCH — изменить статус (issue / mark paid / cancel) или discount.
DELETE /api/wms/invoices/[id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/invoices/[id]/act-data (нет описания)
GET /api/wms/invoices/[id]/act-data — данные для печати акта
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.
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
DELETE /api/wms/invoices/[id]/lines/[line_id] Удаляет строку из draft-инвойса. Пересчитывает total.
DELETE /api/wms/invoices/[id]/lines/[line_id] Удаляет строку из draft-инвойса. Пересчитывает total.
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.
GET /api/wms/kiz (нет описания)
GET /api/wms/kiz?product_id=&client_id=&state=&search=
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.
GET /api/wms/kiz/[id] (нет описания)
GET /api/wms/kiz/[id] — детали + история событий
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. Проверяет валидность переходов.
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.
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
POST /api/wms/kiz/parse body: { code: string }
POST /api/wms/kiz/parse
body: { code: string }
Server-side парсинг (для scan-input). Возвращает извлечённые поля.
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'
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)
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 }
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.
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}]
GET /api/wms/label-templates (нет описания)
GET /api/wms/label-templates?client_id=
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? }
GET /api/wms/label-templates/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/label-templates/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/label-templates/[id] (нет описания)
JSDoc отсутствует в исходнике.
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.
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 должен быть быстрым).
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.
GET /api/wms/labels/templates (нет описания)
GET /api/wms/labels/templates?kind=&client_id=&archived=
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? }
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).
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? }
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.
GET /api/wms/lookup (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/lost-items (нет описания)
GET /api/wms/lost-items — список homeless items. POST /api/wms/lost-items — report new lost item.
POST /api/wms/lost-items (нет описания)
JSDoc отсутствует в исходнике.
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
- Контроль санитарного состояния склада
GET /api/wms/manifests Список manifests текущего ФФ.
GET /api/wms/manifests?status=draft|ready|dispatched|delivered|cancelled Список manifests текущего ФФ.
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.
GET /api/wms/manifests/[id] (нет описания)
GET /api/wms/manifests/[id] — detail с items
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
DELETE /api/wms/manifests/[id] (нет описания)
DELETE /api/wms/manifests/[id] — soft (только если draft)
GET /api/wms/marketplace-credentials Список интеграций. credentials НЕ возвращается (security).
GET /api/wms/marketplace-credentials?client_id= Список интеграций. credentials НЕ возвращается (security).
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 идемпотентен).
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.
GET /api/wms/movements (нет описания)
GET /api/wms/movements?type=&product_id=&request_id=&limit=
GET /api/wms/movements/export (нет описания)
GET /api/wms/movements/export?from=&to=&product_id=&type= → CSV
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.
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 с заявкой.
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.
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.
GET /api/wms/news/[id] (нет описания)
GET /api/wms/news/[id] — single post
PATCH /api/wms/news/[id] (нет описания)
PATCH /api/wms/news/[id] — admin update
DELETE /api/wms/news/[id] (нет описания)
DELETE /api/wms/news/[id]
POST /api/wms/news/[id]/mark-read UPSERT в wms_news_read.
POST /api/wms/news/[id]/mark-read — отметить пост прочитанным текущим юзером. UPSERT в wms_news_read.
GET /api/wms/news/unread-count Возвращает количество неоткрытых постов (published, не expired, в audience scope).
GET /api/wms/news/unread-count — для бейджа на shell-иконке news. Возвращает количество неоткрытых постов (published, не expired, в audience scope).
GET /api/wms/onboarding/state (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/onboarding/state (нет описания)
JSDoc отсутствует в исходнике.
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
GET /api/wms/pallet (нет описания)
GET /api/wms/pallet — list pallets (admin/staff view)
POST /api/wms/pallet (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/pallet/[id]/add-box (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/pdf (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/permissions (нет описания)
GET /api/wms/permissions — каталог всех прав (для UI настроек ролей).
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).
GET /api/wms/products Список товаров с пагинацией + поиск по name/sku + точный поиск по barcode.
GET /api/wms/products?client_id=&search=&barcode=&page=&per_page= Список товаров с пагинацией + поиск по name/sku + точный поиск по barcode.
POST /api/wms/products body: { client_id, sku, name, barcode? }
POST /api/wms/products
body: { client_id, sku, name, barcode? }
GET /api/wms/products/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/products/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/products/[id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/products/[id]/components (нет описания)
GET /api/wms/products/[id]/components — комплектующие товара
POST /api/wms/products/[id]/components body: { component_product_id, qty }
POST /api/wms/products/[id]/components
body: { component_product_id, qty }
DELETE /api/wms/products/[id]/components (нет описания)
DELETE /api/wms/products/[id]/components?component_product_id=X
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 — удалить конкретную привязку.
POST /api/wms/products/[id]/external (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/products/[id]/external (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/products/[id]/locations (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/products/[id]/photos Возвращает массив photos из товара (на случай если list эндпоинт его не вернёт).
GET /api/wms/products/[id]/photos Возвращает массив photos из товара (на случай если list эндпоинт его не вернёт).
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.
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.
DELETE /api/wms/products/[id]/photos/[photo_id] Удаляет фото из JSONB и физически с диска.
DELETE /api/wms/products/[id]/photos/[photo_id] Удаляет фото из JSONB и физически с диска. Если удаляется первое — обновляет image_url.
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 фото на товар.
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.
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.
PATCH /api/wms/products/bulk (нет описания)
JSDoc отсутствует в исходнике.
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 строка.
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 — не создаёт новые.
GET /api/wms/products/export (нет описания)
GET /api/wms/products/export?client_id= → CSV file
POST /api/wms/products/import (нет описания)
JSDoc отсутствует в исходнике.
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`.
GET /api/wms/pvz (нет описания)
GET /api/wms/pvz?marketplace=&active=1&search=
POST /api/wms/pvz (нет описания)
POST /api/wms/pvz — создать ПВЗ
GET /api/wms/pvz/[id] (нет описания)
GET /api/wms/pvz/[id]
PATCH /api/wms/pvz/[id] (нет описания)
PATCH /api/wms/pvz/[id] — частичное обновление
DELETE /api/wms/pvz/[id] (нет описания)
DELETE /api/wms/pvz/[id] — soft delete (is_active = FALSE)
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 с деталями.
POST /api/wms/rejected-events body: { ids: string[] }
POST /api/wms/rejected-events/ack
body: { ids: string[] }
Оператор прочитал/закрыл notification → mark acked_at.
POST /api/wms/rejected-events/record (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/reports/by-client Топ клиентов по объёму операций и выручке за период.
GET /api/wms/reports/by-client?days=30 Топ клиентов по объёму операций и выручке за период. Используется в /wms/reports.
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.
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.
GET /api/wms/reports/finance-summary Финансовый отчёт:
GET /api/wms/reports/finance-summary?days=180&group=month Финансовый отчёт: - выручка по месяцам (timeseries) - разбивка по статусу счетов (draft/issued/paid/canceled) - топ услуг (по сумме за период)
GET /api/wms/reports/timeseries Возвращает объём операций по бакетам времени.
GET /api/wms/reports/timeseries?metric=requests|movements&group=day|week|month&days=30 Возвращает объём операций по бакетам времени. Используется для линейных и stacked графиков на дашборде.
GET /api/wms/reports/warehouse-load Загрузка складов: % занятых ячеек + общее кол-во штук + резерв.
GET /api/wms/reports/warehouse-load Загрузка складов: % занятых ячеек + общее кол-во штук + резерв.
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).
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 если переданы.
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: убрать услугу с позиции
DELETE /api/wms/request-items/[id]/services/[service_id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/requests (нет описания)
GET /api/wms/requests?type=&status=
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).
GET /api/wms/requests/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/requests/[id] Меняет только метаданные. Только в статусе draft.
PATCH /api/wms/requests/[id] Меняет только метаданные. Только в статусе draft.
DELETE /api/wms/requests/[id] (нет описания)
DELETE /api/wms/requests/[id] — только draft.
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'а.
GET /api/wms/requests/[id]/boxes Возвращает коробки прикреплённые к этой партии (FBO supply ↔ box m2m).
GET /api/wms/requests/[id]/boxes Возвращает коробки прикреплённые к этой партии (FBO supply ↔ box m2m).
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).
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 сам в этих статусах не даёт).
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).
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.
POST /api/wms/requests/[id]/cargo-places/[box_id]/orders (нет описания)
JSDoc отсутствует в исходнике.
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 (векторный).
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 }.
GET /api/wms/requests/[id]/consumables Возвращает расходники потраченные на заявку (с остатком на складе).
GET /api/wms/requests/[id]/consumables Возвращает расходники потраченные на заявку (с остатком на складе).
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.
PATCH /api/wms/requests/[id]/consumables/[consumable_id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/requests/[id]/consumables/[consumable_id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/requests/[id]/events (нет описания)
GET /api/wms/requests/[id]/events — audit timeline (newest first).
POST /api/wms/requests/[id]/events body: { notes: string }
POST /api/wms/requests/[id]/events — добавить комментарий.
body: { notes: string }
GET /api/wms/requests/[id]/files (нет описания)
GET /api/wms/requests/[id]/files — список файлов заявки.
POST /api/wms/requests/[id]/files Принимает любой MIME (УПД, накладные, фото-приёмки), сохраняет в
POST /api/wms/requests/[id]/files — multipart upload.
Принимает любой MIME (УПД, накладные, фото-приёмки), сохраняет в
public/uploads/wms/requests/{id}/{uuid}.{ext}
Лимит 20 МБ.
DELETE /api/wms/requests/[id]/files/[file_id] Удаляет запись + физически с диска (best-effort).
DELETE /api/wms/requests/[id]/files/[file_id] Удаляет запись + физически с диска (best-effort).
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.
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>) для дубликата
GET /api/wms/requests/[id]/stages (нет описания)
GET /api/wms/requests/[id]/stages — список этапов заявки
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: смена исполнителя
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.
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 вернёт ошибку, не падаем
молча.
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-кабинете.
GET /api/wms/requests/bulk-import (нет описания)
GET /api/wms/requests/bulk-import — история импортов tenant'а
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 (для отображения)
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.
GET /api/wms/returns Список возвратов FBS текущего ФФ. Сортируем по created_at DESC.
GET /api/wms/returns?status=&client_id=&search=&page=&per_page= Список возвратов FBS текущего ФФ. Сортируем по created_at DESC.
GET /api/wms/returns/[id] Допустимые status: pending → received → processed | rejected | disposed
GET — детали одного возврата. PATCH — изменить status / reason / cell_id / notes. Допустимые status: pending → received → processed | rejected | disposed
PATCH /api/wms/returns/[id] (нет описания)
JSDoc отсутствует в исходнике.
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.
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.
DELETE /api/wms/returns/[id]/photos body: { url: string }
DELETE /api/wms/returns/[id]/photos
body: { url: string }
Удаляет URL из photo_urls (физический файл оставляем — потом cleanup-cron).
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.
POST /api/wms/returns/[id]/qc (нет описания)
JSDoc отсутствует в исходнике.
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' }
GET /api/wms/seller-warehouses Список складов селлера на стороне маркетплейса (для фильтра в FBS).
GET /api/wms/seller-warehouses?client_id=&marketplace= Список складов селлера на стороне маркетплейса (для фильтра в FBS). У клиента может быть несколько: «Коледино Москва», «Волгоград» и т.д. Селлер видит только свои; админ может фильтровать по client_id.
POST /api/wms/send-document (нет описания)
JSDoc отсутствует в исходнике.
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.
GET /api/wms/services (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/services (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/settings/company Возвращает counterparty (юр.лицо/ИП) текущего ФФ. Если у ФФ нет
GET /api/wms/settings/company Возвращает counterparty (юр.лицо/ИП) текущего ФФ. Если у ФФ нет counterparty — создаём stub с именем ФФ, чтобы UI имел что редактировать (counterparty_id NULL → 404, fail loudly).
PATCH /api/wms/settings/company body: любые из полей CounterpartyRow
PATCH /api/wms/settings/company body: любые из полей CounterpartyRow Обновляет counterparty текущего ФФ. Только owner или platform_owner.
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 решает, как часто опрашивать.
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 }.
DELETE /api/wms/settings/max Удалить custom MAX-токен ФФ — вернуться на платформенный fallback.
DELETE /api/wms/settings/max Удалить custom MAX-токен ФФ — вернуться на платформенный fallback. Перед удалением: отменяем webhook subscription в MAX Bot API (best-effort).
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 }.
GET /api/wms/settings/telegram Возвращает public-info о текущей настройке бота ФФ.
GET /api/wms/settings/telegram Возвращает public-info о текущей настройке бота ФФ. Сам токен НИКОГДА не возвращаем (в БД он зашифрован, в API только маскируется).
PATCH /api/wms/settings/telegram body: { token }
PATCH /api/wms/settings/telegram
body: { token }
Установить/обновить токен бота для текущего ФФ.
Валидируем через Bot API getMe → шифруем AES-256-GCM → сохраняем.
Возвращает { username, name } полученные от Telegram.
DELETE /api/wms/settings/telegram Удалить custom-токен ФФ — вернуться на платформенный fallback.
DELETE /api/wms/settings/telegram Удалить custom-токен ФФ — вернуться на платформенный fallback.
POST /api/wms/settings/telegram/test body: { chat_id, text? }
POST /api/wms/settings/telegram/test
body: { chat_id, text? }
Отправить тестовое сообщение через бот ФФ. Используется в UI настройки —
чтобы убедиться что бот добавлен в чат и chat_id правильный.
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).
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.
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.
GET /api/wms/stats Один запрос, все счётчики через подзапросы.
GET /api/wms/stats — агрегаты для дашборда. Один запрос, все счётчики через подзапросы.
DELETE /api/wms/stillages/[id] Полное удаление стеллажа со всеми этажами и ячейками (CASCADE).
DELETE /api/wms/stillages/[id] Полное удаление стеллажа со всеми этажами и ячейками (CASCADE). NB: hard-delete потому что архивных стеллажей не существует — они либо есть, либо нет. На остатках это сейчас не отражается (нет таблицы остатков пока).
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 созданы, либо ни одного.
GET /api/wms/stillages/[id]/structure Возвращает полную схему стеллажа: floors → cells, с подсчётом
GET /api/wms/stillages/[id]/structure Возвращает полную схему стеллажа: floors → cells, с подсчётом boxes/units в каждой ячейке. Для рендера схемы в карте склада.
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).
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).
POST /api/wms/stocks (нет описания)
JSDoc отсутствует в исходнике.
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
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} — начальные остатки`.
POST /api/wms/stocks/import (нет описания)
JSDoc отсутствует в исходнике.
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.
GET /api/wms/subscriptions/current Если подписки нет → возвращаем default (free).
GET /api/wms/subscriptions/current — текущая подписка tenant'а. Если подписки нет → возвращаем default (free).
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 отдельно при подключении).
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 оправдает.
GET /api/wms/tariffs Публичный (auth не нужен) список тарифов для self-service выбора.
GET /api/wms/tariffs?target=seller|ff Публичный (auth не нужен) список тарифов для self-service выбора.
POST /api/wms/transfer (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/transfer/[id]/scan (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/tsd/fbo/scan-bind (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/users (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/users (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/users/[id] (нет описания)
JSDoc отсутствует в исходнике.
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)
PATCH /api/wms/users/[id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/users/[id]/companies Один user может быть привязан к нескольким клиентам (юр.лицам).
GET/POST/DELETE — m2m wms_user_company_link для multi-company access. Один user может быть привязан к нескольким клиентам (юр.лицам).
POST /api/wms/users/[id]/companies (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/users/[id]/companies (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/users/[id]/roles Multi-role: user может иметь несколько ролей одновременно (например
Multi-role: user может иметь несколько ролей одновременно (например 'manager' + 'accountant').
PUT /api/wms/users/[id]/roles (нет описания)
PUT body: { roles: string[] } — replace all
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: текущий месяц.
GET /api/wms/warehouses Возвращает список складов + total.
GET /api/wms/warehouses Возвращает список складов + total.
POST /api/wms/warehouses body: { name: string, address?: string }
POST /api/wms/warehouses
body: { name: string, address?: string }
GET /api/wms/warehouses/[id] (нет описания)
GET /api/wms/warehouses/[id] — карточка склада.
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' }
DELETE /api/wms/warehouses/[id] Soft-delete: status='archived'.
DELETE /api/wms/warehouses/[id] Soft-delete: status='archived'.
GET /api/wms/warehouses/[id]/floor-plan (нет описания)
GET /api/wms/warehouses/[id]/floor-plan — текущая карта склада
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 (только координаты, не создаются — они приходят из конструктора).
GET /api/wms/warehouses/[id]/stillages Список стеллажей склада + total_cells (через JOIN на wms_cell).
GET /api/wms/warehouses/[id]/stillages Список стеллажей склада + total_cells (через JOIN на wms_cell).
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)
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[] }
GET /api/wms/wave (нет описания)
GET /api/wms/wave — list waves (active по дефолту)
POST /api/wms/wave (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/wave/[id] (нет описания)
GET /api/wms/wave/{id} — wave details with all supplies + pick progress per item.
PATCH /api/wms/wave/[id] (нет описания)
JSDoc отсутствует в исходнике.
POST /api/wms/wave/[id]/scan (нет описания)
JSDoc отсутствует в исходнике.
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.
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
POST /api/wms/webhooks (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/webhooks/[id] (нет описания)
JSDoc отсутствует в исходнике.
PATCH /api/wms/webhooks/[id] (нет описания)
JSDoc отсутствует в исходнике.
DELETE /api/wms/webhooks/[id] (нет описания)
JSDoc отсутствует в исходнике.
GET /api/wms/webhooks/[id]/deliveries История попыток доставки для этого webhook'а (последние N).
GET /api/wms/webhooks/[id]/deliveries?limit=50 История попыток доставки для этого webhook'а (последние N).
POST /api/wms/webhooks/[id]/test Отправляет тестовое событие на endpoint webhook'а с подписью HMAC.
POST /api/wms/webhooks/[id]/test
Отправляет тестовое событие на endpoint webhook'а с подписью HMAC.
Возвращает: { status, response, ms } — что вернул их сервер.
Не пишет в wms_webhook_delivery (это дебаг-проба, не реальная доставка).
GET /api/wms/workflow/types (нет описания)
GET /api/wms/workflow/types — список типов заявок текущего ФФ
POST /api/wms/workflow/types (нет описания)
POST /api/wms/workflow/types — создать новый тип заявки
GET /api/wms/workflow/types/[id] (нет описания)
GET /api/wms/workflow/types/[id] — тип + список этапов
PATCH /api/wms/workflow/types/[id] (нет описания)
PATCH /api/wms/workflow/types/[id] — обновить + replace stages если переданы
DELETE /api/wms/workflow/types/[id] (нет описания)
DELETE /api/wms/workflow/types/[id]
Страницы дашборда
{{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\.
-
2026-05-18
ADR — FBO Returns qty_mp Closure (Wave 33)
2026-05-18-fbo-returns-qty-mp-closure.md -
2026-05-18
ADR — FBO WB Hybrid Workflow Architecture (Wave 26-30)
2026-05-18-fbo-wb-workflow-architecture.md -
2026-05-18
ADR — MAX Deep-Link Account Binding Flow (Wave 32)
2026-05-18-max-deep-link-flow.md -
2026-05-18
2026-05-18 — wms-tsd enterprise roadmap (post-0.20.0)
2026-05-18-wms-tsd-enterprise-roadmap.md -
2026-05-16
ADR — Autonomous E2E Hardening (Wave 11-15)
2026-05-16-bcp-autonomous-hardening-wave-11-15.md -
2026-05-16
2026-05-16 — WMS-TSD OTA Self-Update System
2026-05-16-wms-tsd-ota-update-system.md -
2026-05-16
2026-05-16 — WMS-TSD Roadmap (Tier 1-5)
2026-05-16-wms-tsd-roadmap-tier1-5.md -
2026-05-15
🚚 ADR: Driver / Courier handoff flow с GPS + Signature + offline queue
2026-05-15-wms-tsd-driver-handoff.md -
2026-05-15
👥 ADR: Role-aware HomeHub + workflow grouping
2026-05-15-wms-tsd-role-aware-homehub.md -
2026-05-15
🔁 ADR: undo + cascading transition architecture в wms-tsd
2026-05-15-wms-tsd-undo-cascading-architecture.md -
2026-05-14
2026-05-14 — BCP: 1С XML экспорт (упрощённый формат вместо полной ФНС-схемы УПД 5.03)
2026-05-14-bcp-1c-xml-export-simplified.md -
2026-05-14
2026-05-14 — BCP AI integration: 3-provider chain (proxy → OpenAI → OpenRouter)
2026-05-14-bcp-ai-integration-multi-provider.md -
2026-05-14
2026-05-14 — BCP Security Hardening (audit findings batch fix)
2026-05-14-bcp-security-audit-hardening.md -
2026-05-14
ADR: Managed Bots Provisioner — sub-агенты без BotFather rate-limit
2026-05-14-managed-bots-provisioner.md -
2026-05-12
2026-05-12 — AI Office Master Plan
2026-05-12-ai-office-master-plan.md -
2026-05-12
2026-05-12 — Label Generator v2: schema-driven multi-printer architecture
2026-05-12-label-generator-v2-architecture.md -
2026-05-11
ADR: Migration numbering gap 042-050 — intentional jump
2026-05-11-migrations-042-050-intentional-gap.md -
2026-05-09
2026-05-09 — Memory System Redesign: переход с Pensyve на file-based
2026-05-09-memory-system-redesign.md -
2026-05-08
📜 ADR: Обязательное дублирование всего в локальные файлы и GitHub
2026-05-08-knowledge-persistence-rule.md -
2026-05-08
📦 ADR: Batch + sticky-cell UX в Receive (wms-tsd)
2026-05-08-wms-tsd-batch-sticky-cell-ux.md -
2026-05-08
🎨 ADR: WMS Design System для wms-tsd (warehouse-grade UI)
2026-05-08-wms-tsd-design-system.md -
2026-05-07
2026-05-07 — Android-приложение для ТСД (Терминал Сбора Данных)
2026-05-07-android-tsd-app-plan.md -
2026-05-07
2026-05-07 — store bugs fix + Postgres LAN access + DB для напарника
2026-05-07-store-bugs-postgres-lan-team-access.md -
2026-05-06
2026-05-06 — Sentry активация + @sadd_ai_bot для team-алертов
2026-05-06-sentry-and-team-alerts.md -
2026-05-05
2026-05-05 — Deep E2E test marathon + Postgres web GUI
2026-05-05-deep-e2e-and-db-access.md -
2026-05-05
2026-05-05 — Hosting RU: Next.js RCE → cryptominer (RESOLVED)
2026-05-05-hosting-ru-cryptominer-incident.md -
2026-05-04
ADR: BCP/WMS массовый rollout 2026-05-04 — что задеплоено, что НЕ протестировано
2026-05-04-bcp-mega-rollout-pre-acceptance.md -
2026-05-04
🌙 Night-shift отчёт 2026-05-04 — что сделано пока Saddam спал
2026-05-04-night-shift-results.md -
2026-05-03
ADR: Печать-overhaul + bulk-услуги + грузоместа с WB trbx-интеграцией
2026-05-03-print-overhaul-bulk-services-cargo-places-wb-trbx.md -
2026-05-02
ADR: Batch 12 — OZON scopes, токен-rotation, CSV-тарифы, DBS, QR, PgBouncer
2026-05-02-batch-12-features.md -
2026-05-02
ADR: Полный отказ от apiinpacking.ru — локальная BCP layer
2026-05-02-bcp-local-replace-apiinpacking.md -
2026-05-02
ADR: Большой спринт — итерация 2
2026-05-02-big-sprint-2.md -
2026-05-02
ADR: Большой ночной спринт — Stage-panel, KPI, Реестр, Toasts
2026-05-02-big-sprint.md -
2026-05-02
ADR: FBS UX cleanup — WB-aligned pipeline + меньше кнопок/чекбоксов
2026-05-02-fbs-ux-cleanup.md -
2026-05-02
ADR: Универсальный SVG-движок этикеток
2026-05-02-label-svg-engine.md -
2026-05-02
ADR: Mega-batch — auth/audit/webhooks/imports/forecasts/stickers
2026-05-02-mega-batch-features.md -
2026-05-02
ADR: Production blockers — month-close, email, Sentry-lite, DBS
2026-05-02-production-blockers.md -
2026-05-02
ADR: Кабинет селлера — урезанный sidebar для клиентов ФФ
2026-05-02-seller-cabinet-restricted-sidebar.md -
2026-05-02
ADR: Sprint 11/11 — sign-up + reconciliation + admin/health
2026-05-02-sprint-11.md -
2026-05-02
ADR: Ежедневное начисление хранения + журнал
2026-05-02-storage-billing-daily.md -
2026-05-02
ADR: Разведка WB FBS-кабинета + выводы
2026-05-02-wb-fbs-recon.md -
2026-05-02
ADR: Расширенная валидация WB-токена + scope-аналитика
2026-05-02-wb-token-validator.md -
2026-05-01
ADR: Multi-tenant SaaS архитектура для BCP/WMS
2026-05-01-multi-tenant-saas-architecture.md -
2026-05-01
ADR: 2D-карта склада (warehouse floor plan)
2026-05-01-warehouse-floor-plan.md -
2026-05-01
ADR: Workflow Engine — типы заявок + конфигурируемые этапы
2026-05-01-workflow-engine.md -
2026-04-30
2026-04-30 — Cross-platform deep-link для линковки TG ↔ MAX
2026-04-30-cross-platform-deeplink.md -
2026-04-30
2026-04-30 — MAX bot: переход с long-polling на webhook
2026-04-30-max-webhook-migration.md -
2026-04-30
2026-04-30 — Mini App v2: clean 3-tab redesign
2026-04-30-miniapp-v2-redesign.md -
2026-04-30
2026-04-30 — TTS через Microsoft Edge TTS (бесплатно, без API ключей)
2026-04-30-tts-edge.md -
2026-04-27
2026-04-27 — Bot product pivot: коммерциализация (думаем)
2026-04-27-bot-product-pivot-pending.md -
2026-04-27
2026-04-27 — Аудит локальной машины (DESKTOP-OLEK7MM)
2026-04-27-local-machine-security-audit.md -
2026-04-26
2026-04-26 — Critical canonical URL fix
2026-04-26-canonical-fix.md -
2026-04-25
2026-04-25 — Vault & workspace cleanup audit
2026-04-25-vault-cleanup-audit.md -
2026-04-21
2026-04-21 — Security audit: AI VPS + WMS
2026-04-21-security-audit.md -
2026-04-19
2026-04-19 — Agent Core рефакторинг [[02-Projects/ai-telegram-bot]]
2026-04-19-agent-core-refactor.md -
2026-04-19
2026-04-19 — SEO-запуск 5st.pro + новый логотип
2026-04-19-seo-and-logo.md -
ADR-001 — WMS workflow: flexibility-first, не копируем SkladBot
ADR-001-wms-flexibility-first-workflow.md