Skip to content

ТЗ — 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_companiesid, inn, kpp, title, legal_address, credit_limit, lock_versionреквизиты компании, credit_limit — integer minor units, lock_version — optimistic lock
commerce_b2b_employeescompany_id, user_id, role (orderer|approver|viewer)сотрудники компании и роли, role — PHP Enum
commerce_b2b_contractsid, company_id, payment_delay_days, credit_limit_override, price_type_id, lock_versionдоговор с условиями
commerce_b2b_receivablesid, company_id, order_id, amount_due, due_date, status (open|overdue|paid)дебиторская задолженность, status — PHP Enum
commerce_b2b_order_approvalsid, 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/companiesinn, kpp, title, legal_addressFormRequest whitelist; inn — контрольная сумма ИНН (10/12 цифр), автозаполнение остальных полей — предложение из cms/dadata (GET /api/v1/dadata/party/{inn}), не подставляется без подтверждения пользователем
POST /api/v1/admin/b2b/companies/{id}/employeesuser_id, roleFormRequest whitelist, role — PHP Enum (orderer/approver/viewer), user_id должен существовать в ядре
POST /api/v1/admin/b2b/contractscompany_id, payment_delay_days, credit_limit_override, price_type_id, lock_versionFormRequest 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}/decideid (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/dadatahealth-чек / Pulse

Настройки (группа commerce-b2b)

КлючТипДефолтaffectsPageCacheОписание
commerce-b2b.enabledbooltrueнетВключение корпоративных покупателей
commerce-b2b.default_payment_delay_daysint0нетОтсрочка платежа по умолчанию без договора
commerce-b2b.credit_limit_actionstringblockнетРеакция на превышение кредитного лимита: block (жёсткий отказ) или approval (заказ создаётся, требует подтверждения approver)
commerce-b2b.credit_limit_checkbooltrueнетВключение контроля кредитного лимита вообще (при false — лимит не проверяется ни в одном из двух режимов)
commerce-b2b.approval_sla_hoursint24нетОжидаемое время реакции approver на заказ в режиме approval; истечение — алерт, не автоотказ
commerce-b2b.invoice_payment_kill_switchboolfalseнетKill-switch: аварийная блокировка выставления счетов с отсрочкой (способ оплаты «по счёту» скрывается) без выключения модуля — например, при массовой просрочке

Достижение credit_limit/недоступность cms/dadata — понятная ошибка/подсказка и метрика, не 500 и не тихая блокировка.

API

МетодПутьДоступНазначение
POST/api/v1/b2b/companiesauthРегистрация компании покупателя (реквизиты по ИНН)
GET/api/v1/b2b/companies/my/receivablesauth (сотрудник компании)Дебиторская задолженность компании
GET/api/v1/b2b/companies/my/order-approvalsauth (роль approver)Очередь заказов на согласование (режим approval)
POST/api/v1/admin/b2b/order-approvals/{id}/decideauth (approver компании) / admin (commerce-b2b.manage)Подтверждение/отклонение заказа сверх лимита
POST/api/v1/admin/b2b/contractsadmin (commerce-b2b.manage)Создание/правка договора и условий
GET/api/v1/admin/b2b/companiesadmin (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-pricingrequires-сервис (канал 4)commerce-b2b → commerce-pricingрезолв договорной цены resolveForContract(); приоритет над обычной групповой видимостью — см. «Крайние случаи»
cms/dadatarequires-сервис (канал 4, через cms/integrations-bus)commerce-b2b → dadataавтозаполнение реквизитов по ИНН, синхронно с таймаутом и fallback на ручной ввод
cms/commerce-invoicessuggests-сервис (канал 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-b2bresolveOrderPolicy(): роль сотрудника + остаток лимита → разрешить/отказать/на согласование
Очередь commerce-b2bочередь (канал 5)commerce-b2b → commerce-b2b (async)mark-overdue, notify-pending-approvals
cms/healthhealth-чек модуля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 в настройках группыпереключить blockapproval, если бизнес-процесс требует согласования, а не отказа
Согласование зависло, approver не реагируетGET .../order-approvals по компании, commerce-b2b.pending_approvals_count в Pulsecms:commerce-b2b:notify-pending-approvals --json вручную; при отсутствии активного approver — Filament: назначить сотрудника с ролью approver
Дебиторка не переходит в overdue вовремяHorizon/queue:monitor commerce-b2bcms: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 уволен/лишён роли, пока заказ висит в pendingcommerce-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 запрещены

Внутренняя база знаний студии