- pragmarx/google2fa@^9.0 для TOTP RFC 6238.
- AuthController::login изменён: при totp_enabled=true НЕ делает Auth::login,
сохраняет auth.pending_user_id+pending_remember в session, возвращает
requires_2fa=true. /me=401 пока 2FA не пройдена.
- AuthController::verifyTwoFactor: читает pending_user_id, верифицирует TOTP
через Google2FA::verifyKey($secret, $code, window: 1) (окно ±1 = 30s).
Success → Auth::login + regenerate + clear pending + last_login_at.
- VerifyTwoFactorRequest: regex /^\d{6}$/.
- /api/auth/2fa/verify публичный (нет session-auth до verify).
Frontend:
- auth-store::login: при requires_2fa=true user остаётся null (иначе
isAuthenticated=true и guard пустит на /dashboard минуя 2FA).
- auth-store::verifyTwoFactor action.
- api/auth.ts::verifyTwoFactor(code).
- TwoFactorView: onMounted redirect на /login если нет pending state;
submit → verify → /dashboard; на error - clear code + focus first cell.
userEmail из auth.user?.email.
Pest +6 (всего 67/67 за 6.97s, 194 assertions): login для 2FA НЕ создаёт
session + verify success/неверный код/без login/валидация формата +
после verify /me=200.
Vitest +3 (всего 142/142 за 10.75s): login pending vs success state +
verifyTwoFactor success/reject. TwoFactorView spec получил setActivePinia
+ requires2fa=true для bypass onMounted-redirect.
PHPStan baseline +26 Pest TestCall warnings (накопительно).
Регресс: pint+stan passed; vitest 142/142; vite build 908ms;
story:build 21/28 за 31.28s; Pest 67/67 за 6.97s.
CLAUDE.md v1.33->v1.34, реестр Открытых_вопросов v1.42->v1.43.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрыт пункт «pg_partman replacement» из project_phase1_strategy.md
(расширение pg_partman недоступно на native Windows-стеке без сборки
из исходников).
Реализация:
- app/app/Console/Commands/PartitionsCreateMonths.php
- signature: partitions:create-months {--ahead=2}
- создаёт партиции для deals + supplier_lead_costs (обе по received_at)
- идемпотентна (проверка через pg_class WHERE relkind='r' перед CREATE)
- запускать ежесуточно через Windows Task Scheduler / cron
Smoke-test на dev: --ahead=8 создал 6 партиций (Nov 2026 - Jan 2027) +
12 skipped. После migrate:fresh партиции возвращаются к initial 6.
4 новых Pest-теста в PartitionsCreateMonthsTest:
- создание партиций на 8 месяцев вперёд для обеих таблиц
- идемпотентность (повторный --ahead=5 → 0 created, 12 skipped)
- --ahead=0 создаёт только текущий месяц
- INSERT в deals с received_at в новой партиции корректно роутится
Тесты используют beforeEach/afterEach для cleanup'а через
DROP TABLE ... CASCADE (FK webhook_dedup_keys на партицию propagates).
Pest 45/45 зелёные за 4.9 сек. Pint + Larastan чисто (phpstan-baseline
регенерирован для динамических свойств $this в Pest closure'ах).
CLAUDE.md v1.14 → v1.15. Реестр Открытые_вопросы v1.23 → v1.24.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Webhook PoC раскрыл архитектурный пробел в schema v8.6: §5.5-спецификация
делает INSERT в webhook_dedup_keys ДО INSERT в deals (атомарный захват
ключа), но FK был immediate. Решение в две стадии:
1. schema.sql v8.6 → v8.7 — DEFERRABLE INITIALLY DEFERRED на FK
(deal_id, deal_received_at) → deals. ON DELETE CASCADE остаётся
immediate. В bare-транзакции production worker'а решает проблему.
2. Pivot Job на pg_advisory_xact_lock — Pest-тесты с DatabaseTransactions
trait всё равно падали: PG проверяет deferred FK на RELEASE SAVEPOINT,
не на outer COMMIT. Воспроизведено standalone PHP-скриптом, это
PG-семантика subtransactions. Advisory lock работает identically
в любой вложенности транзакций. DEFERRABLE FK сохранён в schema
как defense-in-depth для batch-импортов без savepoint.
Backend стек:
- app/app/Models/Deal.php — composite PK через override
setKeysForSaveQuery (PG требует id+received_at для partition pruning)
- app/app/Models/WebhookDedupKey.php — мини-модель для тестов и debug
- app/database/factories/DealFactory.php — fake данные с received_at
в текущей партиции
- app/app/Jobs/ProcessWebhookJob.php — advisory-lock-based upsert
по §5.5 v8.7. PoC scope: dedup + balance check + project findOrCreate.
TODO для следующих ветвей: BalanceTransaction, SupplierLeadCost,
ActivityLog, RejectedDealsLog, DuplicateDetector (Биз-19).
- app/tests/Feature/DealModelTest.php — 6 тестов composite PK + связи
- app/tests/Feature/ProcessWebhookJobTest.php — 6 тестов: новая сделка,
дубль vid, balance=0, изоляция тенантов, findOrCreate проекта,
ON DELETE CASCADE.
Pest 31/31 за 2.7 сек. Pint + Larastan чисто (phpstan-baseline регенерирован,
scanFiles _ide_helper_models.php добавлен в phpstan.neon).
Документы:
- db/CHANGELOG_schema.md §W (две стадии решения)
- narrative §2.4/§5.5/§6.5/§11 синхронизированы под advisory lock
- Реестр Открытые_вопросы v1.20 → v1.21
- CLAUDE.md v1.11 → v1.12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Первый 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>
Реализованы кейсы 1 + 4 из Прил. И Г.1 «CTO-13: RLS smoke-test через
PgBouncer» как первая проверка RLS-фундамента schema v8.6 ДО первого
PR с tenant-моделью.
app/tests/Feature/RlsSmokeTest.php (NEW):
- кейс 1 (× 2 теста): SET LOCAL app.current_tenant_id изолирует SELECT
в deals — оба тенанта видят только свои 2 deals из 4 общих.
- кейс 1 расширенный: RLS работает на projects (не только deals) —
тот же tenant-контекст применяется ко всем 36 политикам.
- кейс 4: WITH CHECK блокирует INSERT в projects с чужим tenant_id —
ожидается QueryException (RLS WITH CHECK violation).
Стек теста:
- testing-роль `testing_rls_user` NOLOGIN (создаётся идемпотентно через
DO $$ ... IF NOT EXISTS $$). На dev superuser обходит RLS — поэтому
через SET LOCAL ROLE переключаемся на NOLOGIN-роль без BYPASSRLS.
- DatabaseTransactions trait вместо RefreshDatabase — каждый тест в
транзакции, ROLLBACK сбрасывает SET LOCAL ROLE и тестовые данные.
- Отдельная БД liderra_testing (создана `CREATE DATABASE` через psql,
мигрирована `DB_DATABASE=liderra_testing artisan migrate:fresh` 743 ms).
- phpunit.xml: DB_CONNECTION sqlite → pgsql, DB_DATABASE liderra_testing.
Pest 6/6 passed (RlsSmokeTest 4/4 + ExampleTest 2/2) за 723 ms total.
Кейсы НЕ покрытые (отложены):
- Кейс 2-3 (PgBouncer transaction-pooling reuse, job retry): production-
только, на native Windows-стеке нет PgBouncer
- Кейс 5 (REVOKE на 6 saas-таблицах для crm_app_user): требует ролей
из db/02_grants.sql, на dev не созданы (только postgres-superuser)
Сопутствующие правки:
- .gitleaks.toml: + allowlist path для app/tests/*.php (фиктивные
телефоны вида +79000010001 в фикстурах — не реальные ПДн)
- app/phpstan-baseline.neon: regenerated — Pest dynamic $this properties
($this->tenant1Id и т.п.) не парсятся PHPStan без pest-extension,
занесены в baseline (12 entries) до миграции на typed-properties
- CLAUDE.md §6: Pest 2/2 → 6/6, добавлено упоминание CTO-13 smoke-test
- memory project_state.md, MEMORY.md: smoke-test реализован
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Установлены 3 dev-инструмента из Прил. Н фазы 1:
- laravel/pint v1.29 (форматтер PHP, Прил. Н #11) — Laravel preset
по умолчанию. Команды: composer pint, composer pint:test
- larastan/larastan v3.9.6 + phpstan/phpstan v2.1.54 (статанализ
PHP, Прил. Н #12) — level 5, paths app/bootstrap/config/database/
routes/tests. Команда: composer stan
- barryvdh/laravel-ide-helper v3.7.0 (IDE-stubs, Прил. Н #14) —
команда: composer ide-helper. Артефакт _ide_helper.php в gitignore
Конфиги:
- app/phpstan.neon — base config с includes на larastan extension
и baseline. Включены checkOctaneCompatibility и checkModelProperties
- app/phpstan-baseline.neon — зафиксированы 3 ошибки в default
Laravel scaffold (UserFactory return type covariance + 2× ExampleTest
always-true). Двигаемся вверх с этого baseline'а.
Composer scripts (composer.json):
- pint, pint:test (форматирование / dry-run проверка)
- stan (PHPStan analyse с memory-limit 512M)
- ide-helper (generate + meta)
Smoke-test'ы все 3 прошли: Pint passed, PHPStan passed (с baseline),
ide-helper:generate написал _ide_helper.php.
Фикс окружения: на этой машине composer require падал с "Permission
denied на vendor/composer/tmp-XXX.zip" из-за race condition
параллельных curl'ов. Решение: COMPOSER_MAX_PARALLEL_HTTP=1
(сохранено в memory feedback_environment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>