Тема
ТЗ — B2B (юрлица/договоры) (cms/commerce-b2b)
Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Работа с корпоративными покупателями: компании с реквизитами, сотрудники компании с ролями, договоры с индивидуальными условиями и счета на оплату вместо онлайн-платежа.
- Профиль компании: реквизиты по ИНН (автозаполнение через
cms/dadata, синхронный вызов с таймаутом и graceful fallback на ручной ввод) - Сотрудники компании с ролями (кто может оформлять заказы от лица компании)
- Договоры с условиями: отсрочка платежа, кредитный лимит, персональный тип цены (
cms/commerce-pricing) - Счета на оплату (инвойс) как дополнительный способ оплаты рядом с online-эквайрингом —
cms/commerce-invoices - Контроль дебиторской задолженности по компании и договору
- Заказ от лица компании с проверкой прав сотрудника и остатка кредитного лимита — режим реакции на превышение лимита настраиваемый (
block/approval, см. «Настройки»)
Зависимости и выключение
requires: ядро, cms/commerce-model (заказы) · suggests: cms/commerce-pricing, cms/commerce-invoices, cms/dadata (автозаполнение реквизитов по ИНН)
Поведение при выключении: корпоративные покупатели оформляют заказы как обычные физлица-пользователи — договорные условия (отсрочка, лимит, персональные цены) перестают применяться, ранее выставленные счета остаются доступны для просмотра.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
commerce_b2b_companies | id, inn, kpp, title, legal_address, credit_limit, lock_version | реквизиты компании, credit_limit — integer minor units, lock_version — optimistic lock |
commerce_b2b_employees | company_id, user_id, role (orderer|approver|viewer) | сотрудники компании и роли, role — PHP Enum |
commerce_b2b_contracts | id, company_id, payment_delay_days, credit_limit_override, price_type_id, lock_version | договор с условиями |
commerce_b2b_receivables | id, company_id, order_id, amount_due, due_date, status (open|overdue|paid) | дебиторская задолженность, status — PHP Enum |
commerce_b2b_order_approvals | id, order_id, company_id, requested_by, approved_by (nullable), status (pending|approved|rejected), reason (nullable) | заказ сверх лимита в режиме approval — отдельная сущность согласования |
company_id/user_id/order_id/price_type_id/requested_by/approved_by — FK constrained()->index(), индекс по due_date/status под планировщик просрочки, частичный индекс status = 'pending' на commerce_b2b_order_approvals под очередь согласований approver'а.
ПДн-паспорт. Хранит: commerce_b2b_employees.user_id (ссылка на пользователя ядра, без дублирования контактных данных — телефон/email сотрудника читаются из профиля пользователя ядра, не копируются в таблицы модуля); commerce_b2b_companies — реквизиты юрлица (ИНН/КПП/юр. адрес) — это данные организации, не физлица, но связка «сотрудник ↔ компания» персональна. Срок хранения: реквизиты компании и договоры — бессрочно, пока компания активна (юридически значимые документы ссылаются на них); связка сотрудника с компанией удаляется при увольнении/отвязке (явное действие в Filament), архивная копия — в commerce_b2b_order_approvals/дебиторке остаётся user_id-ссылка как часть финансовой истории заказа. Участие в 152-ФЗ: хук ядра «выгрузить всё по субъекту» отдаёт список компаний, где пользователь — сотрудник, и его роль; хук «забыть по запросу» отвязывает user_id от commerce_b2b_employees (запись сотрудника деактивируется), исторические requested_by/approved_by в commerce_b2b_order_approvals и заказы дебиторки не удаляются (финансовый аудит) — ссылка на пользователя в них обезличивается по общему правилу «забыть», не по правилам этого модуля отдельно.
Входные и выходные данные
Whitelist-принцип (§11 стандарта): всё, что не перечислено во «входах», модуль обязан отвергать — неизвестные поля регистрации компании, лишние ключи импорта, произвольные роли сотрудника.
Входы
| Источник | Поля | Чем валидируется |
|---|---|---|
POST /api/v1/b2b/companies | inn, kpp, title, legal_address | FormRequest whitelist; inn — контрольная сумма ИНН (10/12 цифр), автозаполнение остальных полей — предложение из cms/dadata (GET /api/v1/dadata/party/{inn}), не подставляется без подтверждения пользователем |
POST /api/v1/admin/b2b/companies/{id}/employees | user_id, role | FormRequest whitelist, role — PHP Enum (orderer/approver/viewer), user_id должен существовать в ядре |
POST /api/v1/admin/b2b/contracts | company_id, payment_delay_days, credit_limit_override, price_type_id, lock_version | FormRequest whitelist, price_type_id — существующий тип цены cms/commerce-pricing (requires-проверка), lock_version → 409 при конфликте |
Оформление заказа от лица компании (cms/commerce-model, вызов сервиса resolveOrderPolicy()) | company_id, user_id, order_total (из корзины) | сервис проверяет роль сотрудника и остаток кредитного лимита — не принимает сумму заказа с клиента, пересчитывает от позиций корзины |
POST /api/v1/admin/b2b/order-approvals/{id}/decide | id (route), decision (approve|reject), reason (обязателен при reject) | FormRequest whitelist, permission commerce-b2b.manage или роль approver компании, Idempotency-Key |
cms:commerce-b2b:import-legacy --source=<профиль> | построчно: company_external_id, inn, kpp, title, credit_limit, contract_external_id, payment_delay_days | схема профиля импортёра + whitelist полей, --dry-run до применения |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Личный кабинет B2B (зона 3) | карточка компании, список сотрудников, дебиторка | GET /api/v1/b2b/companies/my/receivables, конверт {data, meta} |
cms/commerce-pricing (requires, канал 4) | price_type_id договора | прямой вызов PricingService::resolveForContract() — не запись в его таблицы |
cms/commerce-invoices (suggests, канал 4/событие) | реквизиты компании (ИНН/КПП/адрес) для документа | сервис-вызов при генерации счёта/акта; НДС-документ формируется cms/commerce-invoices, этот модуль — источник реквизитов, не генератор PDF |
| Шина событий | B2bCompanyRegistered, B2bContractSigned, B2bCreditLimitExceeded, B2bReceivableOverdue, B2bOrderPendingApproval, B2bOrderApprovalDecided | канал 1, payload — см. «События и обмен» |
| Filament-отчёт | компании/договоры/дебиторка/очередь согласований | keyset-таблица, CSV-экспорт |
cms/health | метрики отставания очереди, доступность cms/dadata | health-чек / Pulse |
Настройки (группа commerce-b2b)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
commerce-b2b.enabled | bool | true | нет | Включение корпоративных покупателей |
commerce-b2b.default_payment_delay_days | int | 0 | нет | Отсрочка платежа по умолчанию без договора |
commerce-b2b.credit_limit_action | string | block | нет | Реакция на превышение кредитного лимита: block (жёсткий отказ) или approval (заказ создаётся, требует подтверждения approver) |
commerce-b2b.credit_limit_check | bool | true | нет | Включение контроля кредитного лимита вообще (при false — лимит не проверяется ни в одном из двух режимов) |
commerce-b2b.approval_sla_hours | int | 24 | нет | Ожидаемое время реакции approver на заказ в режиме approval; истечение — алерт, не автоотказ |
commerce-b2b.invoice_payment_kill_switch | bool | false | нет | Kill-switch: аварийная блокировка выставления счетов с отсрочкой (способ оплаты «по счёту» скрывается) без выключения модуля — например, при массовой просрочке |
Достижение credit_limit/недоступность cms/dadata — понятная ошибка/подсказка и метрика, не 500 и не тихая блокировка.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/b2b/companies | auth | Регистрация компании покупателя (реквизиты по ИНН) |
| GET | /api/v1/b2b/companies/my/receivables | auth (сотрудник компании) | Дебиторская задолженность компании |
| GET | /api/v1/b2b/companies/my/order-approvals | auth (роль approver) | Очередь заказов на согласование (режим approval) |
| POST | /api/v1/admin/b2b/order-approvals/{id}/decide | auth (approver компании) / admin (commerce-b2b.manage) | Подтверждение/отклонение заказа сверх лимита |
| POST | /api/v1/admin/b2b/contracts | admin (commerce-b2b.manage) | Создание/правка договора и условий |
| GET | /api/v1/admin/b2b/companies | admin (commerce-b2b.view) | Список компаний с фильтром по задолженности (keyset-пагинация) |
Ошибки — конверт {message, code, errors} с обязательным code: credit_limit_exceeded, employee_role_forbidden, company_not_yours (IDOR-защита), approval_pending.
Компоненты
Виджеты: карточка компании и список сотрудников в личном кабинете B2B, очередь заказов на согласование для роли approver. Filament: справочник компаний, договоров и условий, реестр дебиторки с просрочками, массовые действия (перевод пачки компаний на другой price_type_id, массовая деактивация просроченных договоров). Команды: cms:commerce-b2b:mark-overdue --json (пересчёт статуса просроченной задолженности), cms:commerce-b2b:notify-pending-approvals --json (напоминание approver'ам по SLA), cms:commerce-b2b:import-legacy --source=<профиль> --dry-run --json.
Демо-контент: сидер создаёт демо-компанию с сотрудниками трёх ролей, договором и дебиторкой в разных статусах (open/overdue/paid) — виджеты видны в /_gallery и playground без ручного ввода.
События и обмен
| Событие | Когда | Payload |
|---|---|---|
B2bCompanyRegistered | компания зарегистрирована | company_id, inn |
B2bContractSigned | договор оформлен/изменён | contract_id, company_id |
B2bCreditLimitExceeded | заказ сверх кредитного лимита (в обоих режимах — block и approval) | company_id, order_amount, credit_limit, action (block|approval) |
B2bOrderPendingApproval | заказ создан и ждёт согласования (режим approval) | order_id, company_id, requested_by |
B2bOrderApprovalDecided | заказ подтверждён/отклонён approver'ом | order_id, company_id, decision, approved_by |
B2bReceivableOverdue | задолженность просрочена | receivable_id, company_id, due_date |
Слушает: — (реагирует на собственные действия оформления заказа). Прямой сервис-вызов PricingService::resolveForContract() из cms/commerce-pricing по requires для договорного типа цены; сервис-вызов cms/dadata (GET /api/v1/dadata/party/{inn}, suggests) для автозаполнения реквизитов по ИНН.
Таблица взаимодействий (пять каналов — обмен данными):
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/commerce-pricing | requires-сервис (канал 4) | commerce-b2b → commerce-pricing | резолв договорной цены resolveForContract(); приоритет над обычной групповой видимостью — см. «Крайние случаи» |
cms/dadata | requires-сервис (канал 4, через cms/integrations-bus) | commerce-b2b → dadata | автозаполнение реквизитов по ИНН, синхронно с таймаутом и fallback на ручной ввод |
cms/commerce-invoices | suggests-сервис (канал 4) | commerce-invoices → commerce-b2b | чтение реквизитов компании при генерации счёта/акта; commerce-b2b не вызывает invoices напрямую — invoices тянет данные при OrderPlaced |
ЭДО-провайдер (cms/edo, если установлен) | provides/suggests через cms/commerce-invoices | цепочка: commerce-b2b → invoices → edo | реквизиты компании доходят до ЭДО-документа транзитом через invoices; при отсутствии cms/edo в парке — документооборот остаётся ручным, commerce-b2b не блокируется |
cms/commerce-model (заказы) | requires обратный (заказ вызывает политику b2b) | commerce-model → commerce-b2b | resolveOrderPolicy(): роль сотрудника + остаток лимита → разрешить/отказать/на согласование |
Очередь commerce-b2b | очередь (канал 5) | commerce-b2b → commerce-b2b (async) | mark-overdue, notify-pending-approvals |
cms/health | health-чек модуля | commerce-b2b → cms/health | отставание очереди, доступность cms/dadata, накопление pending-согласований сверх SLA |
| Filament / admin API, личный кабинет | REST, внешний канал | admin/approver ↔ commerce-b2b | договоры, дебиторка, согласования |
Фоновая работа
Именованная очередь commerce-b2b: cms:commerce-b2b:mark-overdue по расписанию (планировщик переводит дебиторку в overdue строго по due_date, без ручного вмешательства), cms:commerce-b2b:notify-pending-approvals — напоминание approver'ам о заказах, ожидающих решения дольше approval_sla_hours. Внешние вызовы (DaData) — синхронные с таймаутом и graceful fallback (автозаполнение — подсказка, не блокирует регистрацию компании при недоступности).
Эксплуатация (ранбук). Метрики: commerce-b2b.receivables_overdue_amount, commerce-b2b.pending_approvals_count, доля заказов с credit_limit_exceeded, отставание очереди commerce-b2b (Pulse). Алерты: pending_approvals_count с возрастом
approval_sla_hours(согласование зависло),cms/dadataнедоступен N подряд ретраев, ростreceivables_overdue_amountвыше порога (конфиг studio-парка, не модуля).
| Симптом | Что проверить | Чем чинить |
|---|---|---|
Заказы сотрудников не проходят с credit_limit_exceeded, хотя ожидалось согласование | commerce-b2b.credit_limit_action в настройках группы | переключить block → approval, если бизнес-процесс требует согласования, а не отказа |
| Согласование зависло, approver не реагирует | GET .../order-approvals по компании, commerce-b2b.pending_approvals_count в Pulse | cms:commerce-b2b:notify-pending-approvals --json вручную; при отсутствии активного approver — Filament: назначить сотрудника с ролью approver |
Дебиторка не переходит в overdue вовремя | Horizon/queue:monitor commerce-b2b | cms:commerce-b2b:mark-overdue --json вручную, проверить расписание в ScheduleRegistrar |
| Автозаполнение по ИНН не работает | cms:dadata:doctor --json (модуль cms/dadata) | форма продолжает работать с ручным вводом (fallback уже есть) — чинить cms/dadata, не commerce-b2b |
| Договорная цена не применяется сотруднику компании | cms:commerce-pricing:doctor — резолвер видит price_type_id договора | проверить commerce_b2b_contracts.price_type_id существует и активен в cms/commerce-pricing |
Бэкап/рестор. В бэкап — все 5 таблиц целиком (commerce_b2b_receivables, commerce_b2b_order_approvals — финансово значимые, обязательны). После рестора пересоздаётся командой: агрегаты дебиторки — cms:commerce-b2b:mark-overdue --json (идемпотентно приводит статусы к актуальным по due_date). Рестор без её прогона не считается завершённым.
Производительность и кеш
Ожидаемые объёмы: сотни компаний, тысячи договоров, десятки тысяч записей дебиторки на крупный B2B-магазин. Горячий путь — резолв договорной цены сотрудника при каждом просмотре каталога/карточки товара: company_id сотрудника и price_type_id активного договора читаются из кеша группы commerce-b2b:company:<id> (0 запросов на горячем пути), сам резолв цены по типу — зона ответственности cms/commerce-pricing (кешируется им по сегменту). Проверка кредитного лимита при оформлении — не горячий путь просмотра, но обязана быть 1 запрос (агрегат открытой дебиторки компании по индексу (company_id, status)), не пересчёт по всей истории заказов на каждый чих.
Критичные индексы: unique inn на commerce_b2b_companies; составной (company_id, status) на commerce_b2b_receivables под контроль лимита и планировщик просрочки; индекс due_date под mark-overdue; частичный индекс status = 'pending' на commerce_b2b_order_approvals под очередь approver'а.
Теги кеша commerce-b2b:company:<id>, commerce-b2b:contract:<id>. Инвалидация — событиями B2bCompanyRegistered/B2bContractSigned и правкой в Filament. Дебиторская задолженность, реквизиты компании и договорная цена — персональные/коммерческие данные: карточка каталога с договорной ценой сотрудника, страница «моя компания»/дебиторка не попадают в общий page-cache — сегментация только флагом personalized у блока/фрагмента (механизм ядра, ревизия контрактов), самодельная сегментация запрещена.
Безопасность
Роль сотрудника (orderer/approver/viewer) проверяется на каждом оформлении заказа от лица компании — viewer не может создать заказ. Реквизиты компании (ИНН/КПП/адрес) — через FormRequest-whitelist, автозаполнение DaData не подставляется без подтверждения пользователем (предложение, не автоприменение).
IDOR. Сотрудник одной компании не видит данные другой: все эндпоинты личного кабинета (.../my/receivables, .../my/order-approvals) скоупятся по company_id сотрудника из RequestContext/токена, не по параметру запроса — попытка передать чужой company_id в query/body отклоняется на уровне Policy (company_not_yours), не 404 (не раскрывает существование чужой компании — как и с персональными промокодами в cms/commerce-promo, ошибка не должна давать оракул для перебора).
Кредитный лимит — контроль на уровне сервиса, не только UI. Проверка выполняется на сервере при каждом оформлении заказа от суммы пересчитанной корзины, не суммы, переданной клиентом (та же дисциплина, что «скидка пересчитывается на сервере» в cms/commerce-promo) — сумма заказа с фронтенда никогда не источник истины для сравнения с лимитом.
Разграничение прав: commerce-b2b.view, commerce-b2b.manage.
Матрица ролей (permission/действие × роль, дефолт):
| Действие | Админ | Менеджер | Редактор | Studio | Сотрудник компании |
|---|---|---|---|---|---|
commerce-b2b.view (справочник компаний, дебиторка) | ✅ | ✅ | — | ✅ | — |
commerce-b2b.manage (договоры, лимиты, массовые действия) | ✅ | — | — | ✅ | — |
| Оформление заказа от лица компании | — | — | — | — | orderer/approver |
| Подтверждение заказа сверх лимита | — | ✅ (эскалация) | — | ✅ | approver |
| Просмотр данных своей компании | — | — | — | — | orderer/approver/viewer |
Изменение кредитного лимита и режима credit_limit_action — финансово значимое действие, только commerce-b2b.manage (менеджер видит дебиторку, но не меняет условия договора).
UX-требования
Админ. Работа со списком компаний/дебиторкой: фильтр по задолженности/просрочке, пустое состояние «компаний ещё нет» с подсказкой пригласить первого B2B-клиента. Массовые действия: перевод пачки компаний на другой price_type_id, массовая деактивация договоров с истёкшим сроком. Человеческие ошибки: «нельзя удалить компанию — есть открытая дебиторка» вместо голого 422; «DaData недоступна — заполните реквизиты вручную» вместо зависшей формы. Подтверждение необратимых операций: удаление/архивация компании с открытой дебиторкой — предупреждение с суммой задолженности.
Покупатель (B2B-сотрудник). Понятная причина отказа заказа сверх лимита — не просто отказ, а по режиму: в block — «превышен кредитный лимит компании, обратитесь к менеджеру», в approval — «заказ отправлен на согласование, ожидайте подтверждения ответственного сотрудника» (не тупиковое сообщение, заказ создан и виден в кабинете со статусом pending_approval). approver видит очередь заказов на согласование с суммой, инициатором и остатком лимита компании прямо в карточке решения. Способ оплаты «счёт с отсрочкой» на чекауте — дополнительный пункт рядом с онлайн-оплатой при активном договоре с отсрочкой, а не замена (если online-эквайринг тоже разрешён для этой компании), с подсказкой срока оплаты по счёту.
Крайние случаи и типовые баги
- Договорная цена vs групповая видимость типа цены. Сотрудник компании с
price_type_idв договоре одновременно состоит в пользовательской группе с другой видимой ценой (например, оптовая по группе) → резолверcms/commerce-pricingвызывается с явнымprice_type_idдоговора (resolveForContract()), договорная цена приоритетнее обычной цепочки видимости по группе/лояльности — это разведено на уровне вызова (b2b передаёт конкретный тип, а не полагается на общий резолв по контексту пользователя). - Превышение лимита в режиме
approval, затем повторное превышение до решения по первому заказу → каждый заказ сверх лимита создаёт свою записьcommerce_b2b_order_approvals, лимит на second-заказ считается от фактически открытой дебиторки (уже оформленные, но не оплаченные заказы, включая ожидающие согласования, учитываются как зарезервированные суммы) — не позволяет обойти лимит параллельными заказами, пока первый висит вpending. - Approver уволен/лишён роли, пока заказ висит в
pending→commerce-b2b:mark-overdue-подобная проверка не требуется, но Filament/admin (commerce-b2b.manage) может решить заказ эскалацией, если в компании не осталось активногоapprover— иначе заказ зависает бессрочно; алерт поapproval_sla_hoursподсвечивает такие случаи оператору. - Несколько плательщиков/грузополучателей у одной компании (головной офис платит, филиал получает) — открытый вопрос, зафиксирован явно, не решается сходу: текущая модель ТЗ — один
company_idна заказ, доставка/оплата настраиваются полями заказа (cms/commerce-model), без отдельной сущности «адрес доставки/плательщик компании». Если сценарий понадобится клиенту — это отдельная сущностьcommerce_b2b_company_locations(плательщик/грузополучатель как под-профили компании) поверх текущей схемы, будущее расширение — подлежит вынесению в открытые вопросы, не реализуется в этой версии модуля. - Счёт с отсрочкой при отключённом online-эквайринге сайта → способ оплаты «счёт» — единственный доступный вариант чекаута для этой компании, UI не показывает пустой список способов оплаты, а сразу — счёт с понятным описанием условий отсрочки.
invoice_payment_kill_switch=trueпри уже висящих неоплаченных счетах → ранее выставленные счета остаются действительными и доступны для оплаты/просмотра (как и при выключении всего модуля), kill-switch блокирует только новые выставления счёта с отсрочкой — оформление заказа деградирует до online-оплаты или отказа, если online недоступен, с понятной причиной.cms/dadataнедоступен при регистрации компании → форма регистрации не блокируется, поля заполняются вручную,B2bCompanyRegisteredиздаётся как обычно — автозаполнение синхронное с таймаутом и graceful fallback (см. «Фоновая работа»).cms/commerce-pricingвыключен, у компании есть договорная цена →suggests-зависимость недоступна: резолв откатывается на дефолтный тип цены (розница) как при выключенииcms/commerce-pricingв его собственном ТЗ — деградация, не 500; сотрудники видят розничные цены до включения модуля обратно.- Конкурентное редактирование договора двумя админами одновременно →
lock_version(optimistic lock) наcommerce_b2b_contracts/commerce_b2b_companies, второй запрос получает 409 и понятное сообщение, не «последний победил» молча. - Ловушка измерения
city_id/site_id: компания и договор не привязаны к городу (юрлицо работает across городов инсталляции), но региональная цена (city_idнаcommerce_prices) резолвится независимо от договорногоprice_type_id— договорной тип цены комбинируется с региональностью так же, как обычный тип (правило измерений §4 стандарта, тест гоняется с мультигородом включённым и выключенным). - ⚠️ Противоречие: исходное ТЗ описывало
credit_limit_checkкак жёсткий отказ, инструкция по углублению требует различатьblock/approval. Разрешение в этом ТЗ:credit_limit_check(bool) сохранён как общий выключатель контроля лимита вообще (приfalseлимит не проверяется), добавлена отдельная настройкаcredit_limit_action(block/approval), определяющая как именно реагировать при включённом контроле — старое поведение (block) остаётся дефолтом,approval— осознанный выбор студии/клиента без миграции данных (только смена enum-значения настройки).
Донорский код
Донор: — (новая разработка).
Миграция legacy-данных. cms:commerce-b2b:import-legacy --source=<профиль> — маппинг данных о юрлицах и договорах со старой платформы клиента (обычно выгрузка из 1С/старой CRM) на commerce_b2b_companies/commerce_b2b_contracts. Идемпотентность — по nullable unique external_id на обеих таблицах: повторный прогон обновляет существующие записи по external_id, не дублирует. --dry-run — отчёт расхождений (сколько будет создано/обновлено/пропущено, включая записи с некорректным ИНН) без записи. Построчные ошибки — скачиваемый отчёт, молчаливый пропуск запрещён. Прогон на копии данных — часть приёмки модуля (§16 стандарта), сотрудники (commerce_b2b_employees) маппятся отдельно по совпадению email/телефона с существующими пользователями ядра — не создаются автоматически без подтверждения администратором.
Тесты и приёмка
- [ ] Контрактный тест: заказ сверх
credit_limitкомпании отклоняется приcredit_limit_check=trueиcredit_limit_action=block - [ ] Заказ сверх
credit_limitприcredit_limit_action=approvalсоздаётся со статусомpending_approval, не отклоняется жёстко - [ ] Решение approver'а (
approve/reject) переводит заказ и записьcommerce_b2b_order_approvalsв терминальный статус,Idempotency-Keyисключает двойное решение - [ ] Сотрудник с ролью
viewerне может оформить заказ от лица компании (проверка прав) - [ ] Сотрудник одной компании не видит данные другой (IDOR): подмена
company_idв запросе →company_not_yours, не раскрывает существование чужой компании - [ ] Договорной тип цены (
price_type_id) применяется резолвером цены вместо обычной групповой видимости для сотрудников компании - [ ] Кредитный лимит проверяется на сервере от пересчитанной суммы корзины, значение с клиента игнорируется
- [ ] Счёт на оплату предлагается дополнительным способом оплаты при активном договоре с отсрочкой, если online-оплата тоже разрешена — не заменяет её принудительно
- [ ]
invoice_payment_kill_switch=trueблокирует новые счета с отсрочкой, не трогая ранее выставленные - [ ] Дебиторка переходит в
overdueпланировщиком строго поdue_date, без ручного вмешательства - [ ] Параллельные заказы сверх лимита не обходят контроль, пока первый висит в
pending_approval(резервирование по открытой дебиторке) - [ ] Конкурентная правка одного договора/компании двумя админами → 409 по
lock_version - [ ] При выключении модуля заказы оформляются на общих условиях, ранее выставленные счета остаются видимы для просмотра
- [ ] ПДн-хук «забыть по запросу» отвязывает
user_idотcommerce_b2b_employees, не удаляя записи дебиторки/согласований - [ ] Права
commerce-b2b.view/commerce-b2b.manageи роли сотрудника (orderer/approver/viewer) разграничены по матрице ролей - [ ]
cms:commerce-b2b:import-legacy --dry-runидемпотентен, отчёт расхождений (включая некорректный ИНН) корректен на копии данных - [ ] Контрактный набор
cms-testingзелёный, пакет тестируется в testbench-изоляции - [ ] Feature-тест на каждый роут API,
cms/dadataв тестах замокан - [ ] Тестовая БД — только
commerce-b2b_test;migrate:fresh/refresh/resetзапрещены