Code-quality review of Task 3: index() filtered by is_active only —
an expired-but-active key would be listed as valid. Adds an
expires_at > now() filter plus a test. Cannot occur today (regenerate
is the only write path, always +1 year) but is the correct semantic
contract for an «active key» listing.
phpstan-baseline.neon: count bumps only for ApiKeyControllerTest.php
($tenant 5→7, $user 3→5, getJson 3→4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit J5/D3: the api_keys table existed in schema but had zero code.
Adds the ApiKey model + factory, and ApiKeyController with GET
/api/api-keys (list active keys, key_hash hidden) and POST
/api/api-keys/regenerate (deactivate prior + create new, full key
returned once, bcrypt-hashed in DB). Tenant-scoped via auth:sanctum +
tenant middleware (RLS on api_keys). phpstan-baseline.neon updated for
Pest PendingCalls false-positives in the new test file; also removes
8 pre-existing stale ignore.unmatched entries (properties now resolved
by existing @mixin IdeHelper* docblocks — confirmed pre-existing via
git stash test before Task 3 changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review of Task 2: documents why ProfileTab needs no
watch-resync of auth.user (router beforeEach awaits fetchMe before
requiresAuth navigation); tightens the save-error test to assert the
exact fallback message instead of mere truthiness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit D1: ProfileTab fields were hardcoded refs and the Save button had
no handler. Rewired to the auth store + a new api/auth updateProfile()
calling PATCH /api/auth/me. Single «Полное имя» field split into Имя +
Фамилия (matches users.first_name/last_name); decorative «Роль» field
removed (no such column). AuthUser type gains phone + timezone.
SettingsView.spec.ts updated: «Полное имя» assertion changed to check
for «Имя» and «Фамилия» (collateral fix for the intentional field split).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review of Task 1: first_name had a 422 test but last_name
(identical required rule) did not. Adds the symmetric test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SyncSupplierProjectsJob:77 has a time-budget guard that breaks the
sync loop after 20:55 Europe/Moscow. Five of the eight tests in
SyncSupplierProjectsJobTest omitted Carbon::setTestNow(), so they
inherited real wall-clock time and silently failed (job no-ops)
every evening after 20:55 MSK -- a latent test bug since dedaae5
(Plan 3), mis-attributed to a Redis race (quirk 72) in earlier audits.
Pins beforeEach to a fixed pre-cutoff clock; the job code is correct
and unchanged. Verified: 8/8 in isolation, full suite back to green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit J6: ProfileTab needs a full-profile update endpoint. Adds
AuthController::updateProfile (first_name/last_name/phone/timezone),
routed in the existing /api/auth auth:sanctum group; mirrors the
sibling updateNotificationPreferences. userResource() now also returns
phone + timezone so the GET /me round-trip carries them.
phpstan-baseline.neon updated for Pest PendingCalls false positives
in the new test file (same pattern as all other Feature test files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit A7: the «Оферта» / «Политика» links in the AuthLayout footer were
raw <a href> pointing at unrouted paths -> 404 via the SPA catch-all.
Adds a single DRY LegalDocView served by /legal/:doc(offer|privacy),
rendering an honest «document being finalized» stub (real legal text
needs юр. редактура — реестр K3 / blocker Б-1). Footer links upgraded
to <RouterLink> for SPA navigation. Also refreshes two stale auth-layout
doc-comments left by the /recovery removal (review M1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit A2/A3: RecoveryCodesView (route /recovery) had a TODO no-op
continue handler and 8 hardcoded mock codes. Recon found the page is
orphaned — nothing in the UI navigates to /recovery. The real 2FA
recovery-codes flow lives entirely in Settings -> Безопасность
(TwoFactorCard setup wizard + RecoveryCodesCard regeneration), both
already wired to the real API. Per user decision (2026-05-15) the
orphan is deleted rather than polished.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fix для commit e0bbf4d (G2 AdminSupplierPricesView errors):
I-2 (load() coverage gap): Добавлен 1 test «load() sets fetchError when
axios.get rejects». Раньше load() error handling (try/catch + fetchError
ref + v-alert warning) реализован но без test coverage. Reviewer flagged
как low-risk gap. Now covered.
Tests 8/8. Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axios.patch теперь в try/catch с extractErrorMessage() helper. Per-row
ошибки — reactive errorMessages: Record<number, string> отображаются как
v-icon mdi-alert-circle с v-tooltip рядом с кнопкой «Сохранить».
Success — v-snackbar (3s timeout, color=success, bottom-right) с именем
поставщика.
Retry на той же строке очищает предыдущий error перед новым axios.patch.
load() тоже обёрнут — fetchError ref + v-alert warning сверху таблицы.
+3 Vitest specs (save error / save success / retry clears error).
Регрессий 0.
Closes audit ID G2 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes для commit 72a0064 (G1 AdminPricingTiersView errors):
I-1 (mock leak risk): Добавлен afterEach(() => vi.clearAllMocks()) в
новый describe block. Раньше axios.isAxiosError.mockReturnValue(true)
оставался активным после run'а нового describe. Сейчас нет других
тестов после G1 describe в файле — но future-proof против перестановки
test order.
I-2 (coverage gap): Оба success теста (submit + confirmDelete) теперь
assert vm.successToastOpen === true. Раньше тест мог пройти, если
кто-то забыл successToastOpen.value = true в impl — message set, но
snackbar не открыт. Now covered.
Tests 9/9. Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
axios.post/delete теперь обёрнуты в try/catch с extractErrorMessage()
хелпером из api/client.ts (same pattern as AdminSystemView.vue:32-45).
errorMessage отображается в v-alert (closable, type=error, tonal),
successMessage — в v-snackbar (color=success, 4s timeout).
На failed submit диалог остаётся открытым чтобы пользователь мог
исправить и повторить (UX-pattern). saving=false гарантированно
сбрасывается в finally.
+4 Vitest specs (submit error / submit success / delete error / delete success).
Регрессий 0.
Closes audit ID G1 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
window.alert блокирует UI thread, не accessible (a11y), breaks браузерный
automation (Playwright/Selenium). Заменено на v-snackbar (timeout 6s,
color warning, location bottom-right, кнопка «Закрыть»). Текст идентичен:
«Применено: N. Пропущено: M (конфликт с уже доставленными лидами).»
+2 Vitest specs (snackbar opens / snackbar НЕ opens at skipped=0).
window.confirm для pause/resume/archive намеренно оставлен — это
deliberate blocking прерывание для деструктивных операций (UX-pattern).
Closes audit ID C5 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes для commit 9068005 (C4 KanbanView DnD persist):
I-2 (test coverage gap): Revert test «onColumnChange reverts...» теперь
seed'ит deal в dealsByStatus['hot'] до вызова onColumnChange (имитируя
vuedraggable mutation pre-event). После failed transition — assert
карточка удалена из hot + восстановлена в new. Раньше array-revert
branch в KanbanView.vue:80-87 (splice + push) имел 0 test coverage —
findIndex возвращал -1, splice silent. Теперь coverage 100%.
I-3 (stale JSDoc): File-header comment в KanbanView.vue lines 7-16
обновлён — описывает actual behavior после Task 2 (optimistic + API call
+ revert). Раньше явно врал «не входит в этот коммит: PATCH /api/deals/
{id}» когда POST /api/deals/transition уже реализован.
Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drag-drop между колонками теперь сохраняется в БД через существующий
DealBulkActionController@transition endpoint (single-element массив).
Optimistic UI update (statusSlug меняется сразу) + revert-on-fail с
toast «Не удалось переместить — восстановлен исходный статус».
Без auth.user.tenant_id (dev/demo без login) — local-only mode, API не
зовётся (graceful degradation).
+3 Vitest specs в KanbanView.spec.ts (success / revert / no-auth skip).
Pest covered by existing DealTransitionTest. Регрессий 0.
Closes audit ID C4 from docs/superpowers/specs/2026-05-15-portal-audit-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review fixes для commit 4e77947 (C2 FilterChip popovers):
I-4 (latent interaction bug): Удалена двойная open-path в FilterChip
activator. v-menu сам управляет projectMenuOpen/managerMenuOpen через
activatorProps. Draft-state копируется при menu open → true через
watch(menuOpen, ...). Раньше:
- Activator click: menuOpen=true
- @click on FilterChip: onRedesignFilterClick → menuOpen=true (duplicate)
- Re-click для close: activator toggles false → onRedesignFilterClick
forces true back → menu не закрывается.
I-2 (inline toggle extract): Multi-line ternary @click заменён на
named methods toggleProjectDraft(proj) / toggleManagerDraft(name).
Консистентно с existing clearProjectDraft / clearManagerDraft. Также
unit-testable независимо от template.
onRedesignFilterClick остаётся для Status chip read-only behavior (P2
backlog Sprint 5). defineExpose обновлён: убран onRedesignFilterClick,
добавлены toggleProjectDraft/toggleManagerDraft/clearProjectDraft/
clearManagerDraft (для будущих spec'ов).
Vitest 3/3 C2-specs обновлены на прямой trigger projectMenuOpen=true
+ $nextTick (watcher seeds draft). Регрессий 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CsvReconcileJobTest used Bus::fake() (all jobs), silencing dispatch_sync of
RefreshSupplierSessionJob when a parallel afterEach wiped supplier:session.
Now: Bus::fake([RouteSupplierLeadJob::class]) + anonymous mock that re-puts
the session in handle(), making race-window recovery deterministic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DatabaseTransactions did not prevent cross-session data accumulation in
liderra_testing; count assertions drifted (1465 managers, 519 projects).
RefreshDatabase runs migrate:fresh once per session (RefreshDatabaseState::migrated)
so stale data is wiped at start of each composer test run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vue-tsc was emitting 9 errors from two issues:
1. ProjectCard.vue had a local `interface Project` missing region_mask /
region_mode / delivery_days_mask, while stores/projectsStore.ts
exported the canonical one with those fields. ProjectsView.vue passed
the canonical Project to ProjectCard handler signatures which expected
the local incomplete one → 5× TS2322.
2. EditProjectDialog passed `project: Project | Record<string, unknown>`
to NewProjectDialog which expected `Record<string, unknown> | null`.
Project lacks an index signature → TS2322.
3. AppSidebar.vue template referenced `item.countKey` not declared in
NavItem interface → 2× TS2339.
Changes:
- ProjectCard.vue: drop local Project, import from projectsStore.
- NewProjectDialog.vue: project prop type → Project | null (was Record).
Drop `as { id: number }` cast on PATCH URL.
- EditProjectDialog.vue: project prop type → Project | null.
- AppSidebar.vue: add `countKey?: string` to NavItem.
- projectsStore.ts: make region_mask/region_mode/delivery_days_mask
optional (backward-compat for mock fixtures; production rows always
populate them by schema).
- Test/story fixtures expanded with delivered_today/is_active/archived_at/
sync_status to match strict Project shape.
Verification:
- npx vue-tsc --noEmit → 0 errors (was 9).
- npx vitest run on 5 affected specs → 16/16 passed + 2 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Корень: dev-БД `liderra` создавалась с LC_CTYPE=C — lower()/upper() не
делает case-folding для кириллицы, `ILIKE '%сп%'` на «Окна СПб» = 0 строк.
Test-БД с Russian_Russia.1251 маскировала проблему.
Системный fix: dev-БД пересоздана через `LOCALE_PROVIDER icu ICU_LOCALE 'und'`
(PG 16+ ICU collation, кросс-платформенно). Точечный COLLATE-workaround не
понадобился — все 5 ILIKE-endpoint'ов теперь работают с кириллицей без
правки кода. CTO-20 закрыт в реестре v1.81; команда CREATE DATABASE с ICU
зафиксирована для prod-deploy.
Сопутствующее:
- ProjectsView clearable: workaround `::after content '✕'` + видимость
через `.v-field--dirty` (mdi-* font не подключён в проекте — CTO-19
заведён в реестре).
- LookupsTest: удалён stale case `GET /api/projects?tenant_id=N`,
заменённый auth:sanctum-роутом в Plan 5.
- Pest +1 регрессионный тест (`search is case-insensitive for Cyrillic`)
в ProjectsListShowTest, 10/10 / 37 assertions.
- phpstan-baseline регенерирован (3 actingAs + удалённый case).
- cspell-words: +Регистронезависимый, +und.
- app/.backups/ в gitignore.
Verify:
- Pest --parallel: 742 passed / 1 flaky error (CsvReconcileJobTest cache
race, в изоляции 2/2 PASS) / 3 skipped.
- Browser: «сп» и «окн» возвращают «Окна СПб».
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-existing failing test from commit c9ee8d8 — data-testid lives on
<label>, but @change handler sits on <input> inside it. jsdom does not
bubble change-event from label to input via @vue/test-utils trigger.
Use child-input selector to fire the event on the right node.
Baseline после fix: 614 passed / 3 skipped / 0 failed (vs 613 / 3 / 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Delta mode combines Add/Remove numeric inputs into a single signed delta;
Replace mode switches to an absolute value input via v-checkbox toggle.
5/5 Vitest pass; full suite 611 passed + 3 skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>