Commit Graph

166 Commits

Author SHA1 Message Date
Дмитрий 768628d914 phase2(7-features): bulk-actions / new-deal / tenant-card / system-edit / webhook / smart-filters / impersonation-list
7-фичный auto-mode пакет согласно «карте что осталось» (после v1.54).

(1) Bulk-actions DealsView:
- dealsState reactive-копия MOCK_DEALS (deep-clone) для безопасного bulk-edit.
- Bulk-bar (sticky, теало-нуар, theme=dark) при selected.length > 0:
  count + Сменить статус (v-menu × 14 lead_statuses) + Экспорт (snackbar) +
  Удалить (v-dialog confirm) + ✕ clear.
- На production: smart status-transition с проверкой allowed-переходов;
  soft-delete (архив 30 дней); реальный CSV/XLSX export через xlsx-lib.

(2) NewDealDialog (used in DealsView+KanbanView):
- 6 полей: name/phone/project (MOCK_PROJECTS) / manager (MOCK_MANAGERS) /
  cost / status (default 'new' или presetStatus). Phone-валидация ≥10 цифр.
- emit('created', deal) → DealsView push в начало dealsState; KanbanView push
  в правильную колонку по statusSlug + totalDeals++.

(3) AdminTenantDetailView (/admin/tenants/:code):
- 4 KPI cards (Баланс/runway / Тариф+MRR/мес / Лиды сегодня+неделя+месяц /
  Средняя цена). 4 v-tabs: Финансы (balance-history) / Пользователи /
  Проекты / Активность с event-кодами.
- Кнопка «Войти как клиент» (использует ImpersonationDialog из v1.54).
  404-fallback. composables/mockTenantDetail.ts с expandTenantDetail.
- AdminTenantsView получил @click:row → router.push.

(4) Edit-flow AdminSystemView (audit-log + 2-step):
- Backend: SystemSetting + SaasAdminAuditLog Eloquent (append-only,
  payload_before/after JSONB casts).
- AdminSystemSettingsController с GET (list) + PUT (update в DB::transaction
  + INSERT в saas_admin_audit_log; hash-chain trigger BEFORE INSERT
  заполняет log_hash).
- Type-validation: int/decimal/bool/json. Reason ≥30 chars. No-op → 422.
- Frontend SystemSettingEditDialog — 3-step (edit → confirm с diff
  before/after → done).

(5) Webhook receive endpoint (POST /api/webhook/{token}):
- WebhookReceiveController::receive. Token = tenants.webhook_token.
- 404 unknown / 422 bad payload / 202 success + dispatch ProcessWebhookJob.
- Stub-INSERT в webhook_log через DB::table обёрнут в DB::transaction +
  SET LOCAL app.current_tenant_id для RLS.
- CSRF-исключение для api/webhook/* в bootstrap/app.php.
- На prod: + HMAC X-Webhook-Signature + per-token rate-limit.

(6) Smart-filters:
- DealsView: multi-select v-select Проект+Менеджер с auto availableProjects/
  availableManagers computed.
- AdminTenantsView: filterStatuses (4 STATUS_OPTIONS) + filterTariffs
  (computed availableTariffs).
- Кнопка «Сбросить» появляется только когда фильтры активны.

(7) AdminImpersonationView (/admin/impersonation):
- Backend +2 GET endpoints: /active (used_at != null AND session_ended_at
  == null) + /recent (last 20 завершённых с duration_seconds через
  abs(diffInSeconds) — Carbon signed по умолчанию).
- ImpersonationToken получил belongsTo(Tenant).
- Frontend view: 2 секции (Активные с end-кнопкой / Недавно завершённые
  read-only) + refresh + onMounted load.
- Маршрут /admin/impersonation + 5-й nav-пункт «Impersonation» в AdminLayout.

Vitest +48 (всего 238/238 за 15.31 сек).
Pest +16 (всего 136/136 за 15.8 сек, 495 assertions).
PHPStan baseline регенерирован (0 errors после фикса nullsafe.neverNull).

Регресс: lint+type-check+format ; vite build 937 ms; Pint+PHPStan passed;
Pest 136/136. Реестр v1.54→v1.55, CLAUDE.md v1.45→v1.46.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 05:33:21 +03:00
Дмитрий 1963694778 phase2(impersonation): backend Ю-1 (init-token + verify + end)
- ImpersonationToken Eloquent (UPDATED_AT=null) + isExpired/isUsable helpers
- ImpersonationController 3 endpoints: init / verify / end
- init: reason ≥30 chars, 6-значный random_int код, bcrypt-hash, TTL 15 мин
- verify: Hash::check; failed_attempts+=1; ≥5 → invalidated_at
- end: session_ended_at; 422 если не было verify
- _dev_plain_code в response (prod заменит email клиенту)
- Routes /api/admin/impersonation/{init,verify,end} (без middleware на MVP)
- TODO: saas-admin auth (Yandex 360 SSO) + two-person approval (CTO-15/Ю-9) + email клиенту + UI dialog в AdminTenantsView
- Pest +9 ImpersonationTest (120/120 за 15.62с, 443 assertions)
- Регресс: lint+type+format OK; build 846ms; Pint+Stan passed
- CLAUDE.md v1.43→v1.44, реестр v1.52→v1.53
- Все 9 пунктов списка v1.46 закрыты (кроме #6 SSO ⏸ Б-1 и #7 browser-mode отложен инфра)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:24:02 +03:00
Дмитрий 2e91469a07 phase2(suspicious-login): email при 3 неудачах login (ТЗ §22.4.4 п.3)
- App\Mail\SuspiciousLoginNotification Mailable + emails.suspicious_login Blade
- maybeNotifySuspiciousLogin срабатывает ровно при count==3 (защита от спама на 4-5)
- Для unknown email — skip (некому)
- На dev MAIL_MAILER=log → письмо в storage/logs

ТЗ §22.4.4 анти-брутфорс закрыт полностью:
  - email-rate-limit 5/15мин (v1.36)
  - IP-lockout 10/час (v1.41)
  - email-notify при 3 неудачах (this commit)

- Pest +4 SuspiciousLoginNotificationTest (111/111 за 14.32с, 401 assertions)
- Регресс: Pint+Stan passed
- CLAUDE.md v1.41→v1.42, реестр v1.50→v1.51

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:10:58 +03:00
Дмитрий d1e4237e2f phase2(ip-lockout): auth_log записи + 10 неудач/час с IP → 429 (ТЗ §22.4.4 п.2)
- AuthController::isIpLockedOut: count login_failed за час с IP, ≥10 → 429 + Retry-After: 3600
- logAuthEvent: 3 ветки failure_reason (invalid_password / unknown_email / account_locked)
- DB::table('auth_log')->insert; hash-chain trigger заполняет log_hash (OPEN-И-15)
- Защита поверх email-rate-limit: один IP не сможет перебирать множество email'ов
- Pest +6 IpLockoutTest (107/107 за 13.86с, 380 assertions)
- Регресс: Pint+Stan passed
- CLAUDE.md v1.40→v1.41, реестр v1.49→v1.50

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:07:17 +03:00
Дмитрий 73e64128dc phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted

Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE

Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)

- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:03:02 +03:00
Дмитрий c39d555e6f phase2(recovery-code): POST /api/auth/2fa/recovery-use + UseRecoveryCodeView
- AuthController::useRecoveryCode перебирает unused codes через Hash::check, нормализация (lowercase + remove dash/space)
- UserRecoveryCode Eloquent (UPDATED_AT=null — schema без updated_at)
- Rate-limit auth:recovery:{pending_user_id}|{ip} (5/15мин)
- Returns recovery_codes_remaining для UI-warning'а (sessionStorage на frontend)
- UseRecoveryCodeView.vue → POST /api/auth/2fa/recovery-use, /recovery-use route, autocomplete=one-time-code
- TwoFactorView "резервный код" ссылка /recovery → /recovery-use
- Pest +6 RecoveryCodeTest (91/91 за 12.77с, 319 assertions)
- Vitest +6 (166/166 за 11.47с)
- TODO: #3 2FA setup wizard (после этого /recovery view получит реальный source данных)
- Регресс: lint+type+format OK; build 849ms; story:build 21/28 за 30.36с; Pint+Stan passed
- CLAUDE.md v1.38→v1.39, реестр v1.47→v1.48

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:43:58 +03:00
Дмитрий 9c488122a1 phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:36:27 +03:00
Дмитрий 170382878b phase2(forgot-password): POST /api/auth/forgot + ForgotPasswordView интеграция
- AuthController::forgotPassword использует Password::sendResetLink (anti-enumeration: всегда 200)
- AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets — указывает на нашу таблицу из schema v8.7
- Rate-limit 5/15мин по auth:forgot:{email}|{ip} — hit ставится ДО sendResetLink (защита перебора через unknown email)
- Frontend: authApi.forgotPassword, auth-store.requestPasswordReset, ForgotPasswordView success-state
- Pest +6 в ForgotPasswordTest (79/79 за 10.55с, 273 assertions)
- Vitest +4 (153/153 за 11.11с)
- TODO: POST /api/auth/reset-password + UI-форма new_password (deep-link)
- Регресс: lint+type+format OK; build 862ms; story:build 21/28 за 32с; Pint+Stan passed
- CLAUDE.md v1.36→v1.37, реестр v1.45→v1.46

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:10:28 +03:00
Дмитрий 75897b1636 phase2(rate-limit): login + 2FA verify (5/15min) + frontend lockout
- AuthController: RateLimiter::hit/clear на login + verifyTwoFactor по ключу email|ip / pending_user_id|ip
- 429 + Retry-After header + JSON retry_after (lockoutResponse helper)
- ТЗ §22.4.4: 5 попыток / 15 мин; success чистит throttle; inactive user тоже расходует попытки
- extractRateLimitRetry в api/client.ts; auth-store.lockoutSeconds; v-alert в LoginView/TwoFactorView
- Pest +6 в RateLimitTest.php (73/73 за 8.07с, 246 assertions)
- Vitest +4 в auth-store + LoginView (149/149 за 12.31с)
- Quirk: wrong-password в тестах ≥8 символов (LoginRequest::min:8) — иначе валидация падает до controller
- Quirk: vi.mock api/client в auth-store.spec — иначе axios.isAxiosError в jsdom возвращает false для plain Error
- TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email при 3 неудачах
- Регресс: lint+type+format OK; build 886ms; story:build 21/28 за 37.19с; Pint+Stan passed
- CLAUDE.md v1.35→v1.36, реестр v1.44→v1.45

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:49:47 +03:00
Дмитрий 374724a7a3 phase2(auth-2fa): TOTP-verify endpoint + TwoFactorView интеграция
- 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>
2026-05-08 20:14:33 +03:00
Дмитрий 04b90afda4 phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
  personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
  + кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
  accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
  маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
  middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.

Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
  validation + last_login_at update + register success/duplicate/без accept +
  me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
  запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).

Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.

CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:41:35 +03:00
Дмитрий 1d4738dfa2 phase1(infra): partitions:create-months — Artisan-команда (замена pg_partman)
Закрыт пункт «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>
2026-05-08 15:46:41 +03:00
Дмитрий 4803fa0200 phase1(webhook): Deal/WebhookDedupKey + ProcessWebhookJob (advisory lock) — CTO-17 addendum
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>
2026-05-08 15:24:55 +03:00
Дмитрий 5560ebbdfd phase1(eloquent): Tenant/User/Project models + SetTenantContext middleware
Первый 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>
2026-05-08 14:29:50 +03:00
Дмитрий c6f9c62da0 phase1(rls-smoke): CTO-13 — Pest 4 RLS smoke-test (4/4 passed, 662 ms)
Реализованы кейсы 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>
2026-05-08 14:00:43 +03:00
Дмитрий 03456833eb phase1(tooling): Pint + Larastan + IDE Helper (Прил. Н #11/#12/#14)
Установлены 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>
2026-05-08 10:18:06 +03:00