Adds withExceptions render callback for ValidationException that forces
JSON 422 response when request matches api/webhook/supplier/* — regardless
of Accept header. Default Laravel behavior is 302 redirect for non-JSON
clients, which strips POST body.
Observed on prod 2026-05-25: 76 of 234 supplier webhook hits got 302 (Location: /),
mostly for non-B-prefix projects (client.carmoney.ru, cabinet.caranga.ru,
cashmotor.ru). Supplier doesn't follow 302 redirects on POST, so the
lead body is lost. This fix ensures supplier always sees a meaningful
422 with errors[] instead of a redirect.
Other routes unaffected (render returns null for non-webhook URLs).
Первый Eloquent-слой над schema v8.6 + middleware для RLS-фильтрации
HTTP-запросов. Pest 19/19 passed (1672 ms): 4 RLS smoke + 8 model
smoke + 5 middleware + 2 default.
app/app/Models/ (NEW Tenant.php, Project.php; MODIFIED User.php):
- Tenant — saas-уровневая модель (БЕЗ RLS, тенант-родитель). Soft Deletes.
hasMany Users, Projects.
- User — переписан под нашу схему: password_hash вместо password,
first_name/last_name вместо name, override getAuthPassword/Name для
Laravel auth-интеграции. Soft Deletes. belongsTo Tenant.
- Project — tenant-aware с RLS. belongsTo Tenant.
app/database/factories/ (NEW TenantFactory, ProjectFactory; MODIFIED
UserFactory):
- TenantFactory: уникальный subdomain через Str::random + дефолты
(timezone Europe/Moscow, locale ru, is_trial true, api_key_limit 5).
- UserFactory: tenant_id через Tenant::factory() chain, email unique
через faker, password_hash через Hash::make.
- ProjectFactory: tenant_id через factory chain, дефолты под schema.sql
(region_mask 255, delivery_days_mask 127, assignment_strategy manual).
app/app/Http/Middleware/SetTenantContext.php (NEW, alias `tenant`):
- Резолюция tenant_id (приоритет): auth()->user() → subdomain (3+ parts
HTTP_HOST) → X-Tenant-Id header (только dev/testing).
- Без контекста → 403 Forbidden с явным сообщением.
- SET LOCAL app.current_tenant_id внутри транзакции (DB::beginTransaction
+ SET LOCAL + next() + commit/rollback). PgBouncer-safe (Прил. И Г.1
кейс 2).
- Зарегистрирован в bootstrap/app.php через $middleware->alias().
app/tests/Feature/ (NEW TenantModelsTest, SetTenantContextTest):
- TenantModelsTest (8 тестов): factories + связи (Tenant→users/projects,
User→tenant, Project→tenant) + проверка User::getAuthPassword override.
- SetTenantContextTest (5 тестов): 403 без контекста, X-Tenant-Id header,
игнор не-числового header, subdomain резолюция через absolute URL,
middleware устанавливает app.current_tenant_id для last query.
Сопутствующие правки:
- app/.gitignore: + _ide_helper_models.php (сгенерированный, как и
_ide_helper.php — не в репо)
- app/phpstan-baseline.neon: regenerated — Pest dynamic methods
($this->get(), withHeaders()) и factory-return-type mismatch (Larastan
v3 не понимает array<string, mixed> vs array<model property of T>) +
Request::user/header (Larastan тип hint мисс) + console.php $this в
Closure — все в baseline до миграции на typed properties / pest extension
- CLAUDE.md §6: Pest 6/6 → 19/19, добавлены модели + middleware
- memory project_state.md, MEMORY.md: обновлены под новый этап
Не сделано в этой сессии (отложено):
- Deal model — composite primary key (id, received_at) + partitioned
parent. Эта модель сложнее по архитектуре Eloquent.
- ide-helper:models -W -M -N запускался — добавил @mixin IdeHelperX
и сгенерил _ide_helper_models.php; но т.к. этот файл gitignored,
@mixin строки удалены из моделей (PHPStan не нашёл бы класс на CI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>