Дмитрий
b1c3efa1e1
fix(projects): #909 СОЗДАТЬ кнопка — apiClient + interim A regions
...
Root causes:
1. Default axios без withXSRFToken не отправлял CSRF header → 419 silent
fail (catch ловил только 422).
2. PDD regions UI (commits 4f60add..f982046) использовал 32-bit маску,
несовместимую с schema's 8-битным CHECK chk_projects_region_mask_range
→ 500 silent fail.
Changes (NewProjectDialog.vue):
- Replace default axios import с apiClient + ensureCsrfCookie +
extractErrorMessage из api/client.ts (same pattern как NewDealDialog).
- await ensureCsrfCookie() перед mutating; apiClient.post/patch.
- Remove regions <v-autocomplete> + selectedRegions ref + inverted
region_mode watcher (interim A — proper 89-codes реализация в Plan 6).
- Add general error banner для non-422 ошибок (419/401/500/network).
- form.region_mask=255 + region_mode='include' (schema default = вся РФ).
Changes (EditProjectDialog.spec.ts):
- Switch mock с default axios на apiClient (cascading from above).
Verified: Pest 742/739/3sk/0, Vitest 758/3sk/0, vue-tsc 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 19:28:33 +03:00
Дмитрий
bfdab40d88
feat(projects): integrate ProjectDetailsDrawer + swap bulk-bar condition >=2
...
Task 8 of project-details-drawer plan (2026-05-14):
- ProjectsView.vue: import ProjectDetailsDrawer + computed
- singleSelectedProject computed (Project|null when selectedIds.size === 1)
- onDrawerClose/onDrawerSaved handlers (clearSelection / fetch)
- Template: BulkActionsBar condition > 0 → >= 2 (mutual exclusion with drawer)
- Template: mount <ProjectDetailsDrawer> with :project / @close / @saved bindings
- Template: .has-drawer class on .projects-view root when single selected
- Style: .projects-view padding-right 480px transition for push effect
- Test: ProjectsView.spec.ts pre-existing 'shows BulkActionsBar' case updated
to assert >=2 contract (selects 2 projects); 14 PDD tests + 3 view tests
+ 1 skip + toolbar tests all green
Vitest: 3 files / 20 passed / 1 skipped / 0 failed
2026-05-14 15:02:33 +03:00
Дмитрий
e39a42cfdf
fix(a11y): admin search inputs — add label prop for accessible name (Pattern H)
...
A11y rescan Pattern H — Vuetify <v-text-field> без `label` prop рендерит
empty `<label id="input-v-NN-label">` (referenced via aria-labelledby).
Pa11y/axe видит unlabelled input на /admin/billing (search «Поиск по
названию или ИНН») и /admin/system (search «Поиск по ключу или описанию»).
Initial naive fix добавил `aria-label="..."` — но ARIA priority говорит
aria-labelledby overrides aria-label, поэтому осталось violation.
Final fix: add `label="Поиск"` prop on VTextField. Vuetify рендерит
floating label с правильным accessible text → axe-core resolves через
aria-labelledby chain successfully. Placeholder сохранён (split: «Поиск»
теперь в label, «по названию или ИНН» / «по ключу или описанию» —
placeholder).
Files:
- AdminBillingView.vue:209-217
- AdminSystemView.vue:130-138
Closes Pa11y «label» violations на 2 admin URLs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 10:07:48 +03:00
Дмитрий
6387706be6
fix(a11y): .sep dot separator contrast 2.92:1 → 5.33:1 (Pattern B)
...
A11y rescan Pattern B — scoped CSS `.sep { color: #92907b; }` повторяется
в 8 компонентах (page-stats / page-meta / hero-meta containers с точкой-
разделителем `·`). На ivory page background #f6f3ec даёт contrast
2.92:1, ниже WCAG 2.1 AA 4.5:1 threshold.
Fix: #92907b → #6b6356 — same warm-grey hue, darker tone, gives
5.33:1 contrast. 8 files:
- views/{DealsView,BillingView,KanbanView,ReportsView}.vue
- components/dashboard/DashboardPageHead.vue
- components/deals/DealDetailHero.vue
- components/admin/tenants/TenantsStatsHeader.vue
- components/admin/tenant-detail/TenantDetailHeader.vue
Closes Pa11y «color-contrast» violations на /dashboard /billing /reports
(8 .sep elements total flagged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 10:07:11 +03:00
Дмитрий
c5242271d7
chore(p3): close P3 tooling and structural mini-fixes
...
Closes Audit #3 P3 batch.
Changes:
1. **knip.config.ts cleanup** — remove 4 stale config hints flagged in
Audit #3 Phase 1B (`ignore: tests/**` redundant since `project` is
`resources/js/**`; `ignoreDependencies` for vitest/@vue/test-utils/jsdom
redundant since knip auto-detects test frameworks). Add `histoire.config.ts`
+ `resources/js/histoire.setup.ts` to entry — closes 2 documented FPs
(histoire.setup.ts + @histoire/plugin-vue unused-flag). Verified:
`npx knip` exits 0 clean.
2. **Admin table actions column header label** — change `title: ''` →
`title: 'Действия'` in:
- TenantsTable.vue (actions column, /admin/tenants)
- AdminSupplierPricesView.vue (actions column, /admin/supplier-prices)
Closes axe-core `empty-table-header` violation seen in Audit #3 Phase 7
on /admin/tenants. Header is now visible in UI (better UX than sr-only
sleight-of-hand).
3. **npm overrides for lodash** in `package.json` — pin `pa11y-ci > lodash`
to ^4.17.21. Verified: `npm ls lodash` resolves to lodash@4.17.23 (latest
4.x; CVE-2021-23337 + GHSA-f23m patched in <4.17.21, our version is above
that). npm audit may still surface advisory ranges as informational.
4. **Decision doc for pgFormatter (Q.HARD.002)** — explicit FIX-DEFER with
3-hypothesis comparison (Strawberry Perl install vs sqlfluff replacement
vs Docker pg_format vs drop SQL formatting). Decision: drop automated
SQL formatting until Б-1 closure; squawk (linter) covers correctness.
Addendum: axe-core .v-overlay-container region landmark — no permanent
axe-core test setup exists, so no whitelist needed at this point.
Verification:
- knip: 0 issues
- vue-tsc: 0 errors
- ESLint: 0 errors
- Vitest: 91 files / 736 passed / 3 skipped (no regressions)
- Vite build: 2.03s
Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 08:38:51 +03:00
Дмитрий
e746b3c9a4
chore(cleanup): dead code removal + DemoSeeder env-conditional + schema header drift
...
Closes Audit #3 P2 batch (knip dead exports/components, DemoSeeder
hygiene, schema header drift).
- Remove app/resources/js/views/admin/AdminPlaceholderView.vue
(unreferenced placeholder view — confirmed via repo-wide grep, only
doc references remain)
- npm uninstall concurrently (no script invoked it; --legacy-peer-deps
for Histoire 1.0-beta.1 peerDep quirk)
- 12 unused exports → internal types (remove `export` keyword):
- api/admin.ts: AdminTenantsStats, ApiTenantMetrics,
ApiAdminBillingSummary, ApiAdminIncidentsSummary
- api/notifications.ts: NotificationEvent
- api/reports.ts: ApiReportType, ApiReportFormat, ApiReportParameters,
ReportCounts, ReportQuota
- composables/mockBilling.ts: TxType
- composables/useStatusPill.ts: StatusPillSlug
All 12 are used INSIDE their own file (response shapes), just not
exported externally — converting to internal types satisfies knip
without losing type-checking inside the file.
- DatabaseSeeder::run() — DemoSeeder runs only in local+testing envs
(`migrate:fresh --seed` in dev now produces demo tenant + admin@demo.local
+ 3 projects + ~14 demo deals; prod environments skip)
- db/schema.sql header line 4: «62 базовые таблицы» → «63 базовые
таблицы (61 regular + 2 partitioned parents: deals + supplier_lead_costs)»
Closes schema header drift finding from Phase 3.
Verification:
- vue-tsc --noEmit: 0 errors
- ESLint on touched files: 0 errors
- Pest --parallel: 742/739/3sk/0 failed (identical to baseline, no regressions)
- 2243 assertions / 34.46s
Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 08:28:44 +03:00
Дмитрий
0c36b7a28d
feat(a11y): migrate Pa11y scope from handoff prototypes to live Vue app
...
Closes Audit #3 sole P1 (F-A11Y-PA11Y-SCOPE-01).
Pa11y was scanning handoff HTML prototypes from liderra_v8_handoff/concepts/
(3 URLs, ~10 contrast violations), NOT the live Vue app. Audit #2 baseline
"0 errors" was inaccurate — real portal was never covered.
Changes:
- pa11y.config.json: now targets http://localhost:8000/ <route> for 7 guest
pages (login, register, forgot, 2fa, recovery, 403, 500)
- pa11y-handoff.config.json: preserves historical handoff baseline as
opt-in (`npm run a11y:handoff`)
- package.json: new `a11y:handoff` script; `a11y` repointed to live target
- RecoveryCodesView.vue: scoped CSS override fixes Vuetify warning-tonal
alert content contrast (2.03:1 → ≥4.5:1, color #0a0700 per Pa11y rec)
- .github/workflows/a11y.yml: new CI job with dev-server lifecycle
(php artisan serve + curl wait-on + Pa11y + screenshot artifact upload)
- docs/audit-baseline-pa11y.md: first live baseline document with per-URL
status, ignore selectors rationale, re-run instructions
Local verification:
- npm run a11y: 7/7 URLs passed (0 violations)
- vue-tsc: 0 errors
- ESLint: 0 errors
- Vitest: 88 files / 683 passed / 3 skipped / 0 failed (no regressions)
Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-14 08:25:14 +03:00
Дмитрий
c8005e0cfc
fix(a11y): Q.DEFER.004 sub-B — AdminSupplierPricesView 9 inputs aria-label
...
3 supplier rows × 3 form controls (cost_rub v-text-field +
quality_score v-text-field + is_active v-switch) = 9 nodes без label —
axe-core критичная label violation.
Fix: :aria-label='${field} для ${supplier.name}' (e.g. 'Cost (₽) для B1 — Сайты и Звонки').
Test coverage: AdminSupplierPricesView.spec.ts 4-й spec проверяет все 9 ожидаемых
aria-label через DOM query.
2026-05-13 00:35:05 +03:00
Дмитрий
143cc458c1
fix(a11y): Q.DEFER.002 sub-B — 12 patterns fixed across 16 auth views
...
Q.DEFER.002 sub-B closure: manual Pa11y audit-pass via Playwright MCP login +
axe-core CDN inject on 16 auth-required views. Found ~13 unique violation
patterns, 12 fixed, 3 deferred to Q.DEFER.004.
ROOT CAUSE found: AdminLayout `<v-navigation-drawer color="secondary"
theme="dark">` resolved to Vuetify default-dark `secondary=#54b6b2` (Teal
mid) instead of liderraForest `#012019` теало-нуар. Switching to direct hex
preserves design intent + restores white-text contrast across all 8 admin
views (~50 nodes color-contrast violations cleared).
Patterns fixed:
1. AdminLayout sidebar palette (8 admin views):
- color="secondary" → color="#012019 " (root cause)
- .brand-sub red #b94837 → #e06155 (3.41 → 5.08)
- .nav-count gray #7a8c87 → #8a9c95 (4.26 → 5.34)
- <v-list nav> + role="navigation" + aria-label (aria-required-children
fix: <v-list role=list> had [role=link] children — undefined для list)
2. DashboardBalance .runway-bar — role="img" (aria-prohibited-attr fix)
3. DashboardKpiRow .delta-up — #2e8b57 → #1b6e3b (4.27 → 6.25)
4. TransactionsTable .tx-amount-up — #2e8b57 → #1b6e3b (same fix)
5. RemindersList .empty-hint — #9a9690 → #6b6356 (2.98 → 5.74; +liderra-muted alignment)
6. KanbanView .kanban-board — tabindex="0" role="region" aria-label
(scrollable-region-focusable fix)
7. ProjectCard:
- .v-progress-linear + :aria-label="Прогресс дневной нормы: N%"
- icon menu :aria-label="Меню действий проекта «...»"
- bulk-select .card-check input :aria-label="Выбрать проект «...»"
8. useStatusPill in_progress #3F7C95 → #2A5A6E (4.07 → 6.11);
useStatusPill.spec.ts sync
9. ProjectsView toolbar select-all input aria-label
10. AdminTenants impersonate v-btn aria-label
11. Global app.css:
`.v-messages, .v-field-label { --v-medium-emphasis-opacity: 0.7; }`
Vuetify default ~0.52 → rendered #7a7a7a/#767471 fails 4.20-4.29:1;
0.7 → rendered ≈#595959 → 7.9:1+ passes WCAG AA.
Re-verified post-fix via axe-core on all affected views: all clean except
DEV-only `.dev-index-num` chip (tree-shaked в prod, not a real violation).
Vitest verified post-fix: 79 files / 614 passed / 3 skipped / 0 failed
(baseline preserved).
3 patterns deferred to Q.DEFER.004:
- DealsTable VDataTable show-select bulk-checkboxes (6 nodes) — Vuetify
slot rewrite needed
- AdminSupplierPrices 9 form inputs — v-text-field/v-switch label props
- Vuetify v-tooltip eager-mount aria-tooltip-name — library-level cosmetic
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:09:48 +03:00
Дмитрий
5cebe2450d
fix(a11y): ForgotPasswordView info-alert contrast 4.18 → 7+ (Q.DEFER.002 full close)
...
Phase 10 audit Pa11y нашёл WCAG2AA G18 contrast 4.18:1 < 4.5:1 на
v-alert type=info variant=tonal в ForgotPasswordView.vue:81 (rate-limit notice).
Diagnosis через Playwright browser_evaluate:
- Vuetify v-alert text-info color: rgb(63, 124, 149) = #3F7C95 (Forest brand info)
- Tonal-variant bg (computed): #ecf2f5 (light blue-grey, 12% tint от info)
- Contrast: #3F7C95 vs #ecf2f5 = 4.18:1
Fix через локальный scoped CSS override:
- Добавлен class="a11y-info-darker" на v-alert
- :deep selector на .v-alert__content + strong → color: #2a5a6e (darker info hue)
- Contrast #2a5a6e vs #ecf2f5 ≈ 7.5:1 (passes WCAG AAA)
- Visual style v-alert tonal сохранён (light bg, info-color border + icon)
Verify:
- npx pa11y --standard WCAG2AA http://127.0.0.1:8000/forgot → No issues found ✅
- npx vitest run ForgotPasswordView.spec.ts → 5/5 passed
Closes Q.DEFER.002 fully (вместе с ErrorView fix fff2dff ).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 20:56:58 +03:00
Дмитрий
cb05657f30
chore(format): prettier --write across 37 .vue/.ts files
...
Phase 1B audit found 48 files failing `prettier --check`. Auto-apply
via `npx prettier --write resources/js/**/*.{ts,vue,css}` produced
style-only changes:
- consistent quote style
- trailing comma normalization
- spaces around : in v-card style="position: relative" attrs
- explicit ; insertion
No semantic changes. No code-behavior changes. Production-code only;
test files batched separately into `test(frontend):` commit.
Verification:
- npx vitest run → 79/79 files, 614/614 + 3 skipped (no regression).
- npx vue-tsc --noEmit → 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 20:24:33 +03:00
Дмитрий
55a9d3fe00
fix(types): unify Project interface + NavItem.countKey + drop legacy Record
...
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 >
2026-05-12 20:15:26 +03:00
Дмитрий
b5849bbd2a
fix(projects): cyrillic ILIKE via PG ICU + clearable workaround
...
Корень: 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 >
2026-05-12 19:25:25 +03:00
Дмитрий
3fc90f12df
feat(projects-ui): Quiet Luxury redesign for card-check + 5 dialog v-text-fields
...
ProjectCard.vue: replace 2px noir solid border on .card-check__box with
1px var(--liderra-line) idle / var(--liderra-line-strong) hover / var(--liderra-teal)
checked. Checked state uses tonal 10% teal bg instead of full fill. Size 20→16px.
Added :focus-visible outline for keyboard nav.
NewProjectDialog.vue: add a local .ld-input-quiet class to all 5 v-text-field
in the dialog (domain / phone / sms keyword / name / daily limit). The class
overrides v-field outline border-color through :deep() to use the tokens.css
1px line / line-strong / teal palette, and sets border-radius to var(--radius-8).
All variant/density/color values come from Vuetify global defaults in
plugins/vuetify.ts:50-54. Includes opacity:1 on every override to neutralize
Vuetify's --v-field-border-opacity 0.38 cascade, plus an explicit error-state
rule with border-color:currentColor to preserve Vuetify's red error border.
Twin elements left out of scope: .toolbar-check__box in ProjectsView.vue,
v-combobox/v-autocomplete/v-btn-toggle inside the same dialog, and the
filter-bar v-select inputs.
Spec: docs/superpowers/specs/2026-05-12-quiet-luxury-elements-1440-896-design.md
Plan: docs/superpowers/plans/2026-05-12-quiet-luxury-elements-1440-896.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 18:07:20 +03:00
Дмитрий
95bba384a1
feat(projects-bulk): select-all toolbar with counter and indeterminate state
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 15:11:21 +03:00
Дмитрий
2f46a3e5ec
feat(redesign): Task 15 — DealsView filterbar + density + StatusPill + hover lift (motion #2,#4)
2026-05-12 10:13:19 +03:00
Дмитрий
35662f7b56
feat(redesign): Task 14 — DashboardView KPI count-up (motion #1 ) + live pulse
2026-05-12 10:06:08 +03:00
Дмитрий
0245f12b51
chore(dev): inject DevIndexBadge for visual feature feedback on localhost
2026-05-12 04:45:18 +03:00
Дмитрий
76b1562593
feat(frontend): Plan 5 Task 11 — polling integration (setTimeout-recursion + backoff)
2026-05-11 19:44:56 +03:00
Дмитрий
1c3989a6df
feat(frontend): Plan 5 Task 10 — EditProjectDialog wrapper + BulkActionsBar + 7 tests
2026-05-11 19:41:53 +03:00
Дмитрий
92082606e3
feat(frontend): Plan 5 Task 8 — ProjectsView + projectsStore (no polling) + 9 tests
2026-05-11 19:38:59 +03:00
Дмитрий
8bc7838f0c
feat(frontend): Plan 5 Task 9 — NewProjectDialog (3 tabs Site/Call/SMS) + story
2026-05-11 19:31:26 +03:00
Дмитрий
c9ee8d866e
feat(frontend): Plan 5 Task 7 — router + nav + regions + ProjectCard + story
2026-05-11 19:31:23 +03:00
Дмитрий
4bc488e940
fix(admin): AdminPricingTiersView strip ISO-suffix from effective_from caption
...
Caption "(с 1970-01-01T00:00:00.000000Z)" → "(с 1970-01-01)".
Slice on optional-chain in template; UI smoke verified via Playwright,
Vitest tests/Frontend/AdminPricingTiersView.spec.ts 5/5 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 16:37:59 +03:00
Дмитрий
174dbae808
feat(billing): Plan 4 Task 11 — TenantChargesController + ChargesTab + CSV export
...
Backend TenantChargesController:
- GET /api/billing/charges — paginated list, filters period (current_month / last_month / 90d) + charge_source.
- POST /api/billing/charges/export — StreamedResponse CSV (BOM + UTF-8) с chunkById(500).
- auth:sanctum + tenant middleware — RLS изолирует tenant_id.
- 6 Pest integration tests (RLS isolation + filters + pagination + CSV export).
Frontend ChargesTab.vue:
- v-data-table-server с paginated load + period/charge_source filters.
- CSV-download через blob → createObjectURL.
- Forest-palette + JetBrains Mono tnum.
BillingView.vue — добавлен tab «Списания» с импортом ChargesTab.
ChargesTab.story.vue + 4 Vitest tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 11:51:13 +03:00
Дмитрий
0f820c4569
feat(admin): Plan 4 Task 10 — AdminSuppliersController + AdminSupplierPricesView (B1/B2/B3 cost editor)
...
Backend AdminSuppliersController:
- GET /api/admin/suppliers — все 3 поставщика (B1/B2/B3).
- PATCH /api/admin/suppliers/{id} — обновляет cost_rub / quality_score / is_active.
- Validation: cost_rub >= 0, quality_score 0..9.99.
- Audit trail saas_admin_audit_log (stub admin via system-supplier@liderra.local ).
- 4 Pest integration tests.
Frontend AdminSupplierPricesView (Vue 3 + Vuetify 3):
- v-data-table 3 строки с inline-editing cost_rub/quality_score/is_active.
- Forest-palette + JetBrains Mono tnum.
- 3 Vitest tests + Histoire story.
Router /admin/supplier-prices route.
Drive-by fix: SupplierProjectFactory.definition() default signal_type
ограничен ['site','call'] — иначе при ->create(['platform' => 'B1']) с
оригинальным random 'sms' нарушается CHECK chk_supplier_projects_b1_not_for_sms
(flaky parallel-pest race condition). Тесты, которым нужен 'sms', продолжают
явно передавать signal_type вместе с B2/B3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 11:28:03 +03:00
Дмитрий
ed5e3f495d
feat(admin): Plan 4 Task 9 — AdminPricingTiersController + AdminPricingTiersView (CRUD 7-tier + audit)
...
Backend AdminPricingTiersController:
- GET /api/admin/pricing-tiers — active + scheduled.
- POST — create 7-tier set с effective_from=DATE_TRUNC('month', NOW()+1 month).
- DELETE /scheduled/{date} — отмена будущей сетки.
- Validation: ровно 7 tier_no 1..7 unique, tier 7 leads_in_tier=null, price>=0.
- Audit trail saas_admin_audit_log на POST + DELETE (через SaasAdminAuditLog
model: payload_before/after, NOT NULL admin_user_id резолвится через стаб
system-pricing@liderra.local + ip_address из $request->ip()).
- 8 Pest integration tests.
Frontend AdminPricingTiersView (Vue 3 + Vuetify 3):
- v-data-table активной сетки + scheduled groups + dialog editor.
- Forest-palette + JetBrains Mono для tnum-цифр.
- 5 Vitest unit tests (tests/Frontend/, авто-импорт Vuetify через vite-plugin).
- Histoire story для preview.
Router /admin/pricing-tiers route (layout 'admin', requiresAuth).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 11:18:01 +03:00
Дмитрий
79ff60ffd9
refactor(frontend): Sprint 4 Phase B/3 — split 2 utility views (audit O-refactor-04 хвост)
...
ErrorView 320→178 (+ ErrorBrand 54 + ErrorIllustration 31 + ErrorActions 55 + ErrorMeta 102).
DashboardView 302→84 (+ DashboardPageHead 65 + DashboardKpiRow 97 + DashboardBalance 124).
State (config, errorCode в ErrorView; range/kpis/balance в DashboardView) остаётся
в parent ради единого route.meta-driven flow + future API-fetch'а (Phase B/1 паттерн).
DashboardPageHead использует Vue 3.5 defineModel<T>() для двусторонней привязки range.
Sub-components читают только props — без Pinia stores (mock-data flow).
Все sub-components <250 строк (acceptance threshold). Shell line counts: 178/84.
ЗАМЕЧАНИЕ по acceptance «0 components >300»: НЕ закрыто полностью. 2 файла остались
выше порога — DealsView 560 + DealDetailDrawer 386. Зафиксировано в Sprint 3 Phase C
commit 6c2f0ce : bulk-action функции (applyBulkStatus/applyBulkDelete/applyBulkExport/
undoBulkDelete/applyBulkRestoreFromTrash) и comment/reminders fetch экспонируются
через defineExpose в Vitest-тестах напрямую — дальнейшая декомпозиция требует
изменения тест-контракта (отдельным flow, не вошло в Sprint 4 Phase B).
Phase B/3 закрывает 8/12 audit-кандидатов O-refactor-04: 3 в Sprint 3 Phase C
(Top-3) + 5 в Sprint 4 Phase B/1+B/2 (admin/layout/billing/security/reminders) +
2 в B/3 (errors/dashboard). Оставшиеся: 2 крупных deals-view (defineExpose-blocked)
+ ImpersonationDialog уже <300 органически.
Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 989 ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 04:53:09 +03:00
Дмитрий
849bc73290
refactor(frontend): Sprint 4 Phase B/2 — split 3 user views (audit O-refactor-04 хвост)
...
BillingView 416→114 (+ BalanceCard 155 + TransactionsTable 113 + InvoicesTable 90
+ billingFormatters 51 composable: formatPlain/formatCost/statusChipColor/
statusLabel/formatLabel/formatIcon/txAmountClass).
SecurityTab 354→39 (+ ChangePasswordCard 17 + TwoFactorCard 218 + RecoveryCodesCard 104
+ SessionsTable 66; auth-store читается напрямую в каждом sub-component).
RemindersView 345→183 (+ RemindersFilters 51 + RemindersList 173;
ReminderDialog уже отдельный с прошлой фазы — служит как ReminderForm).
State (`activeTab`, `editingReminder`, `deletingReminderId` в RemindersView)
остаётся в parent ради единого reload-flow + confirm-dialog'ов. Auth-store
читается напрямую в TwoFactorCard/RecoveryCodesCard через useAuthStore() —
без prop-drilling. Reminders-store читается напрямую в RemindersFilters/
RemindersList.
Все sub-components <250 строк (acceptance threshold). 3 view-shells: 114/39/183.
Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 968 ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 04:46:14 +03:00
Дмитрий
30ef61dff8
refactor(frontend): Sprint 4 Phase B/1 — split 3 admin/layout views (audit O-refactor-04 хвост)
...
3 view'а с >300 строк разделены на shell + sub-components:
AdminTenantsView 377→155 (+ TenantsStatsHeader 82 / TenantsFilters 93 / TenantsTable 116).
AdminTenantDetailView 436→109 (+ TenantDetailHeader 158 / TenantDetailTabs 176
+ adminTenantDetailFormatters 43 composable).
AppLayout 466→78 (+ AppSidebar 155 / AppTopbar 269; R0.6 hard-стоп снят
явным запросом заказчика 10.05.2026).
State (filterStatuses, tenantsState, activeTab, tenant, drawerOpen) остаётся
в parent view'ах ради `defineExpose`-контракта Vitest тестов. Sub-components
читают Pinia stores напрямую (auth + notifications + reminders) — без
prop-drilling.
AppTopbar 269 строк <300 — acceptance threshold выдержан (можно дальше split на
NotificationsDropdown + UserMenu в отдельном flow, не критично).
Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 1.17 сек.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 04:38:08 +03:00
Дмитрий
6c2f0ce682
refactor(frontend): Sprint 3 Phase C — Top-3 Vue components split (audit O-refactor-04)
...
Sprint 3 Phase C. Закрытие audit O-refactor-04 (частичное — Top-3 из 12):
- DealsView.vue: 852 → 560 строк. Выделены: DealsFilters (123), DealsBulkBar (150),
DealsTable (165). CSV-utilities (csvEscape/triggerCsvDownload/triggerBlobDownload/
buildCsvString) вынесены в composables/useCsvDownload.ts.
- ReportsView.vue: 592 → 261 строк. Выделены: ReportRequestForm (тип отчёта,
даты, фильтры, формат, submit/reset), ReportJobsList (список заданий со
статусами и actions retry/cancel/delete).
- DealDetailDrawer.vue: 580 → 386 строк. Выделены: DealDetailHero (header +
phone-link + status-chip), DealDetailTimeline (activity log с MOCK_EVENTS).
Comment- и Reminders-секции оставлены inline — связаны с API и defineExpose.
DealsView и DealDetailDrawer остались выше 350-целевого уровня: bulk-action
функции (applyBulkStatus/applyBulkDelete/applyBulkExport/undoBulkDelete/
applyBulkRestoreFromTrash) и comment/reminders fetch — экспонируются через
defineExpose в Vitest-тестах напрямую, дальнейшая декомпозиция требует
изменения тест-контракта (отдельным flow).
Layout-структуры (AppLayout 466, AuthLayout, AppShell) НЕ ТРОНУТЫ — R0.6 hard-стоп.
Остальные 9 components >300 строк (AdminTenantDetailView, BillingView,
AdminTenantsView, SecurityTab, RemindersView, ErrorView, DashboardView,
ImpersonationDialog, далее) — вне scope Sprint 3, отдельным flow по запросу.
vue-tsc: 0 errors. ESLint: 0. Vitest: 416/416 PASS. Build: success (1.15s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 20:28:25 +03:00
Дмитрий
2d7d7d1188
feat(frontend): Sprint 2 Phase B — Vue 3.5 defineModel + Vuetify 3.12 typed slots + lazy-imports + ESLint check
...
Sprint 2 Phase B (modernization). Закрытие audit O-stack-04/05/07 + O-perf-06:
- O-stack-04: Vue 3.5 defineModel() в 3 диалогах (NewDealDialog,
ImpersonationDialog, ReminderDialog) — boilerplate −5 строк/файл.
+ useTemplateRef() в TwoFactorView (input v-for refs).
- O-stack-05: Vuetify 3.12 типизированные слоты VDataTable
(DealsView + AdminTenantsView) — inline-аннотации `{ item }: { item: T }`
на 6+7 scoped-slot bindings; vue-tsc проверяет доступ к полям статически.
- O-stack-07: ESLint flat-config verified — header-comment добавлен
в eslint.config.js. Legacy .eslintrc.json не используется.
- O-perf-06: defineAsyncComponent() для тяжёлых диалогов в 3 местах:
DealsView (DealDetailDrawer + NewDealDialog), DealDetailDrawer
(ReminderDialog), RemindersView (ReminderDialog). KanbanView оставлен
sync — async-загрузка приводила к EnvironmentTeardownError в jsdom
(KanbanView.spec.ts), see in-file comment. Сборка показывает chunk'и
ImpersonationDialog (7.61 kB), DealDetailDrawer (11.12 kB), NewDealDialog
и ReminderDialog как отдельные lazy-bundles.
vue-tsc: 0 errors. ESLint: 0. Vitest: 416/416 PASS. Build: success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 19:36:02 +03:00
Дмитрий
cab1f87efd
phase2(admin-tenant-detail-frontend): replace mock на real API в AdminTenantDetailView
...
- api/admin.ts +getAdminTenantDetail(subdomain) + 5 типов (ApiTenantUser/Project/
BalanceTx/ActivityEvent/Metrics + AdminTenantDetailResponse).
- composables/adminTenantDetailMapper.ts: mapAdminTenantDetail (API → mockTenantDetail
format). code=subdomain, deriveStatus (trial/overdue/suspended), deriveTariff
(Trial fallback), users (fullName из first+last||email, role='manager' хардкод —
schema users role нет, расширим в Post-MVP), projects (slug=tag), balanceHistory
(id префикс TX-, type-mapping для chargeback_*/trial_bonus/historical_import →
ближайший UI-эквивалент), activity (actor=actor_email||system, summary из
context.from→to), activitySinceText (relative time из last_activity_at).
- AdminTenantDetailView.vue: replace mock-lookup на async loadTenant + 3 ветки
template (loading / notFound / fetchError) + watch(code) для реактивной
навигации. inn/contact_phone/legal_address скрываются через v-if (нет в schema).
- AdminTenantDetailView.spec.ts переписан с MOCK на vi.mock('api/admin'):
13 тестов (вызов API с subdomain / organization_name+tariff / 4 KPI / KPI Лиды
todayActual/desired / Финансы tab / Пользователи tab / Проекты tab / Активность
tab с actor+summary / Войти как клиент / suspended disabled / 404 fallback /
500 fetch-error / overdue Просрочка / trial без оплаты).
- adminTenantDetailMapper.spec.ts +20 тестов: code/name/inn-empty/balanceRub
parse/mrrRub trial-null/status (4 ветки)/tariff (deriveTariff+fallback)/today
Actual+Desired/users (fullName / fallback)/projects/balanceHistory (TX- prefix +
chargeback type mapping)/activity (actor+summary)/metrics (4 поля)/activitySince.
- Vitest +23 (всего 416/416, +23 от 393).
Этап B эпика AdminTenantDetailView (frontend) ЗАКРЫТ. Эпик закрыт целиком (2 этапа).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 14:37:45 +03:00
Дмитрий
e0ffe7e686
phase2(reports-stage4): frontend integration ReportsView (replace mock)
...
- api/reports.ts: типизированные axios-helpers (listReportJobs/createReportJob/
retryReportJob/cancelReportJob/deleteReportJob) + ApiReportJob/Status/Format/
Counts/Quota interfaces. ensureCsrfCookie на mutating-вызовах.
- composables/reportsMapper.ts: mapApiReportJob (API → UI mock format с конверсией
pending→queued / processing→running). title строится на frontend'е (тип + period
с RU-месяцами «апр 2026» или диапазон «мар 2026 — апр 2026»). sizeText форматирует
bytes (B/KB/MB). timeText зависит от status (в очереди / в работе · Nс / N мин назад).
uiTypeToApi (deals → deals_export и т.д.).
- ReportsView.vue полностью переписан под API:
- onMounted → loadJobs (replace MOCK_JOBS на data из listReportJobs).
- usePolling 30 сек (фоновый авто-refresh).
- Submit → createReportJob → reload + success-alert + error-alert (validation+
общие ошибки извлекаются через extractValidationErrors/extractErrorMessage).
- canSubmit computed: disable если квота заполнена (active >= max).
- Reset-btn возвращает форму к defaults.
- Reload-btn (manual fast-path).
- Retry/Cancel/Download/Delete-кнопки → соответствующие API-вызовы;
Delete через v-dialog persistent confirm.
- fetch-error-alert на listReportJobs reject.
- Empty-state «Нет отчётов» когда jobs.length=0.
- canRetry проверяет retry_count<3 (max attempts CTO-6).
- Vitest +24 (всего 393/393, +24 от 369):
- reportsMapper.spec.ts +14: status mapping (pending/processing/done/failed) /
title (один месяц / диапазон) / format / sizeText (B/KB/MB/null) / attempt /
error / timeText (pending / processing / done «10 мин назад» / «только что») /
uiTypeToApi 4 slug'а / progress=50 для running.
- ReportsView.spec.ts переписан с MOCK_JOBS на vi.mock('api/reports') +12:
mount + listReportJobs called on mount / 4 type cards / default Сделки active /
4 формата / quota-banner из API / empty-state / done с Готов+Скачать /
failed с Ошибка+Повторить / failed retry_count=3 НЕ показывает Повторить /
pending с Отменить / Submit вызывает createReportJob+reload / Submit error →
submit-error-alert / Submit-btn disabled при квоте 3/3 / Reset / Reload-btn /
fetch-error-alert / Retry-btn / Cancel-btn / Delete confirm-dialog +
deleteReportJob.
Этап 4/4 эпика Reports backend ЗАКРЫТ. Эпик закрыт целиком.
Backend: 1 type (deals_export) × 4 формата (CSV/XLSX/JSON/PDF-stub).
Этап 2b (3 оставшихся типа: managers_summary/sources_summary/billing_summary)
— расширение через добавление 3 новых Provider-классов без изменений в архитектуре,
вынесено в Post-MVP backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 13:49:55 +03:00
Дмитрий
dc1457a008
phase2(reminders-frontend): RemindersView + DealDetailDrawer + nav-badge
...
P0 этап 5 — frontend для reminders (после backend-этапа 4).
Пользователь может создавать/просматривать/завершать/удалять напоминания
из UI с inline-create в DealDetailDrawer.
Frontend:
- api/reminders.ts: типизированные helpers для 5 endpoints + ensureCsrfCookie
для mutating. Types ReminderFilter/ApiReminder/ReminderCounts.
- stores/reminders.ts: Pinia с items/counts/currentFilter +
load/refreshCounts/create/update/complete/remove. Optimistic для
complete/remove с revert на reject.
- components/reminders/ReminderDialog.vue: dual-mode (create/edit) modal
с native datetime-local input. Props dealId?/reminder? (edit),
ISO-конверсия при submit.
- views/RemindersView.vue: page-head с stats (active/overdue) + reload-btn;
4 tabs (today/upcoming/overdue/completed) с counts на бейджах
(overdue=error color); v-list с complete-btn / dropdown
Изменить/Удалить с confirm-dialog; empty-state.
- router: /reminders маршрут (lazy).
- AppLayout: nav-badge «Напоминания» биндит count из store
(replace static «12»); скрыт при count=0; polling 60 сек для
refreshCounts.
- DealDetailDrawer: секция «Напоминания» (только при tenantId+deal):
inline + create-btn / список / complete / встроенный ReminderDialog.
Vitest +20 (369/369 за 21.20 сек):
- reminders-store 11: initial / load+reject / refreshCounts /
create+reject / complete optimistic+revert / remove+reject / reset.
- RemindersView 7: mount / 4 tabs / counts / empty-state /
список / reload-btn / filter=today default.
- AppLayout +2: бейдж скрыт при count=0 / показывает count при >0.
Pest 347/347 (без изменений — backend нетронут).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 12:41:41 +03:00
Дмитрий
f55b91cfa4
phase2(notifications-stage3): NotificationsTab schema-aligned + prefs API
...
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только
локально без API. Backend events не совпадали с handoff'ом.
Backend:
- PATCH /api/auth/me/notification-preferences под auth:sanctum.
- Replace-семантика: незадекларированные events/channels отбрасываются.
- userResource расширен: notification_preferences + sound_enabled.
- UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT,
DB-DEFAULT JSONB виден как null без явного override).
- Pest +10: 401 / replace / неизвестные events/channels отбрасываются /
422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace-
семантика (отсутствующие events исчезают).
Frontend:
- api/auth.ts: типы NotificationChannel/EventKey/Preferences +
updateNotificationPreferences helper. AuthUser получил optional поля.
- NotificationsTab.vue переписан под schema:
8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/
invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email,
НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер
и тесты mount-then-find падают). dirty через computed-сравнение с
originalPrefs snapshot. save async + success/error alerts.
- SettingsView.spec.ts: legacy event-имена → schema-aligned.
- Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют /
читает prefs из user / save calls API + alerts / Отменить возвращает.
cspell-words: +prefs.
PHPStan baseline регенерирован.
Pest 315/315 (+10) за 36.73 сек, 1130 assertions.
Vitest 349/349 (+10) за 20.42 сек.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 11:41:35 +03:00
Дмитрий
830a652588
phase2(trash-bin): GET /api/deals?only_deleted + «Корзина» в DealsView
...
Расширяет stages 5/6 (soft-delete + 8-сек undo) до постоянного доступа
к удалённым сделкам через отдельный view-mode.
Backend (DealController::index):
- Новый query-param only_deleted=true.
- withTrashed() + whereNotNull('deleted_at') — обход global scope
SoftDeletes + явный фильтр для NO-OP idempotency.
- Все остальные фильтры применимы и в trash-mode.
Pest +3 (DealIndexTest):
- only_deleted=true → только soft-deleted (alive скрыты).
- Без only_deleted → soft-deleted скрыты (default behavior).
- RLS+app-фильтр изолирует чужие удалённые.
Frontend:
- ListDealsParams.onlyDeleted?: boolean + axios mapping.
- DealsView: trashMode ref + toggleTrashMode (clear selected + reload) +
applyBulkRestoreFromTrash (optimistic remove + bulkRestoreDeals + toast).
- UI changes в trash-mode:
- Заголовок «Сделки» → «Корзина».
- Toggle-btn 'mdi-arrow-left К сделкам' (warning-flat) вместо
'mdi-trash-can-outline Корзина' (outlined).
- Скрыты Экспорт + Новая сделка.
- Скрыт chiprow filter-bar.
- Info-alert «Корзина: показаны удалённые сделки».
- Bulk-bar: только Восстановить (mdi-restore success-tonal) + clear;
status/export/delete скрыты.
Vitest +2 (DealsListIntegration):
- toggleTrashMode → trashMode=true + listDeals с onlyDeleted=true.
- applyBulkRestoreFromTrash → bulkRestoreDeals + remove from state +
toast «Восстановлено 2».
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 321/321 за 19.60 сек (+2 от 319).
- Vite build 1.04 сек.
- Pint + PHPStan passed.
- Pest 269/269 за 29.12 сек (+3 от 266, 1009 assertions).
Реестр v1.72→v1.73 / CLAUDE.md v1.63→v1.64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 10:27:11 +03:00
Дмитрий
01c20e7b6c
phase2(polling): usePolling composable 30 сек + Page Visibility pause
...
Закрывает последний unblocked production-TODO «Polling/SSE для real-time».
Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель.
Composable (composables/usePolling.ts):
- usePolling(loader, {intervalMs=30_000, enabled=true}).
- Page Visibility API: при document.hidden=true interval останавливается;
при visibilitychange с возвратом hidden=false — restart + немедленный
loader() (не ждать следующего interval'а).
- Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
- enabled=false — composable не стартует (feature-flag).
Integration:
- DealsView + KanbanView → loadDeals.
- AdminTenantsView → loadTenants.
- AdminBillingView → loadBilling.
- AdminIncidentsView → loadIncidents.
Vitest +6 (usePolling.spec.ts) с vi.useFakeTimers:
- Вызов каждые intervalMs / default 30 сек / skip при document.hidden /
cleanup на unmount / enabled=false → no-op / visibilitychange
pause+resume с немедленным loader.
Регресс:
- Lint+type-check+format passed.
- Vitest 319/319 за 18.67 сек (+6 от 313).
- Vite build 899 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.62 сек (backend не тронут).
Реестр v1.71→v1.72 / CLAUDE.md v1.62→v1.63.
ВСЕ unblocked production-TODO закрыты.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 10:17:51 +03:00
Дмитрий
fa11c7b223
phase2(admin-tenants-mrr): mrr_rub в /api/admin/tenants (этап 7)
...
Закрывает gap из v1.66 — mock-форма имеет mrrRub, но API возвращал null.
Теперь AdminTenantsView показывает реальную колонку MRR.
Backend (AdminTenantsController::index):
- Добавлено tariff_plans.price_monthly as tariff_price_monthly в select.
- mrr_rub в response: price_monthly (string) если не-trial; иначе null.
- Aggregate-формат как у /admin/billing — string чтобы decimal не терял
точность при передаче через JSON.
Pest +3 (AdminTenantsIndexTest):
- mrr_rub='990.00' для активного тарифа не-trial.
- mrr_rub=null для trial (даже если тариф есть).
- mrr_rub=null если current_tariff_id отсутствует.
Frontend:
- ApiAdminTenant.mrr_rub: string | null в типе.
- mapApiAdminTenant: parseFloat(api.mrr_rub) или null (вместо hardcoded
null из v1.66).
- AdminTenantsView: formatRub(item.mrrRub) для консистентности с другими
₽-полями.
Vitest +2:
- mrr_rub строка → number.
- mrr_rub=null → mrrRub null.
PHPStan baseline регенерирован. cspell-glossary +консистентности.
Регресс:
- Lint+type-check+format passed.
- Vitest 313/313 за 18.83 сек (+2 от 311).
- Vite build 947 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.39 сек (+3 от 263, 1001 assertion).
Реестр v1.70→v1.71 / CLAUDE.md v1.61→v1.62.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 10:08:12 +03:00
Дмитрий
c34d4009d1
phase2(restore-flow): POST /api/deals/restore + undo-snackbar (этап 6 — completion of stage 5)
...
Soft-delete был half-done: пользователь не мог отменить случайное удаление.
Теперь после bulk-delete показывается snackbar «Удалено N · Восстановить»
на 8 секунд.
Backend (DealController::restore):
- POST /api/deals/restore {tenant_id, ids: [1..1000 ints]}.
- withTrashed() обходит global scope SoftDeletes + явный
whereNotNull('deleted_at') для NO-OP idempotency на живых.
- RLS + defense-in-depth where(tenant_id).
- ActivityLog event=deal.restored, context.source='bulk' для каждой
ВОССТАНОВЛЕННОЙ. Константа EVENT_DEAL_RESTORED добавлена в модель.
Pest +7 (DealRestoreTest):
- 422/404 базовые / soft-delete + restore + audit / NO-OP на живых
не пишет audit / defense-in-depth (свой restored, чужой остался) /
после restore видна в GET /api/deals / 422 пустой массив.
Frontend:
- dealsApi.bulkRestoreDeals — POST-helper.
- DealsView::applyBulkDelete: snapshot удалённых сделок (deep-clone
manager.*) сохраняется в lastDeletedSnapshot ref.
- undoBulkDelete() async: optimistic re-insert + bulkRestoreDeals если
auth.user; success → toast «Восстановлено N»; fail → warning.
- v-snackbar bulk-delete: 3→8 сек timeout + #actions слот с кнопкой
«Восстановить» (показ только при snapshot.length > 0). После undo
snapshot очищается → кнопка пропадает.
Vitest +3 (DealsListIntegration):
- bulk-delete + undo восстанавливает обе + bulkRestoreDeals + cleanup
snapshot.
- Undo без tenant_id — НЕ вызывает API + только локально.
- Undo reject → warning toast + локальное восстановление остаётся.
PHPStan baseline регенерирован. cspell-glossary +unshift +партиальный.
Регресс:
- Lint+type-check+format passed.
- Vitest 311/311 за 18.71 сек (+3 от 308).
- Vite build 877 ms.
- Pint + PHPStan passed.
- Pest 263/263 за 27.68 сек (+7 от 256, 998 assertions).
Реестр v1.69→v1.70 / CLAUDE.md v1.60→v1.61.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 10:01:35 +03:00
Дмитрий
f0dce283a8
phase2(soft-delete): schema v8.9 + DELETE /api/deals (этап 5/5 — авто-план закрыт)
...
Bulk soft-delete для UI applyBulkDelete. Hard-delete отбракован из-за
CASCADE-FK от webhook_dedup_keys: hard уничтожил бы dedup-ключи и
нарушил идемпотентность webhook §5.5.
Schema v8.8 → v8.9:
- deals.deleted_at TIMESTAMPTZ (NULL = живая).
- Partial index (tenant_id, status) WHERE deleted_at IS NULL —
самый частый UI-фильтр.
- ALTER TABLE на партиционированной deals distributes во все 6
партиций автоматически (PG 14+).
- CHANGELOG +§U с обоснованием soft vs hard.
Backend (DealController::destroy):
- DELETE /api/deals {tenant_id, ids: [1..1000 ints]}.
- Bulk-update deleted_at=NOW() через RLS + defense-in-depth where(tenant_id).
- ActivityLog event=deal.deleted (source='bulk') для каждой ИЗМЕНЁННОЙ.
- NO-OP (уже удалена) не пишет audit.
- Deal model: SoftDeletes trait + deleted_at в fillable/casts. Global
scope автоматически добавляет whereNull('deleted_at') ко всем существующим
query (index/show/transition/update/export).
Pest +8 (DealDestroyTest):
- 422/404 базовые / soft-delete + audit / defense-in-depth (свой
удалён, чужой жив) / NO-OP idempotency / GET скрывает soft-deleted
(list+show 404) / 422 пустой массив.
- Quirk: migrate:fresh --env=testing без .env.testing использует liderra
вместо liderra_testing → решение DB_DATABASE=liderra_testing migrate:fresh.
Frontend:
- dealsApi.bulkDeleteDeals — DELETE-helper с config.data (axios особенность).
- DealsView::applyBulkDelete async: optimistic local-removal +
bulkDeleteDeals если auth.user; success → toast «Удалено N из M.»;
fail → warning toast + локальный update НЕ откатывается.
Vitest +3 (DealsListIntegration):
- bulkDeleteDeals с tenant_id + optimistic + toast.
- Без tenant_id — НЕ вызывается.
- Reject → warning toast + локальный update остаётся.
PHPStan baseline регенерирован.
АВТО-ПЛАН (5 этапов) ЗАКРЫТ ПОЛНОСТЬЮ.
Регресс:
- Lint+type-check+format passed.
- Vitest 308/308 за 20.12 сек (+3 от 305).
- Vite build 973 ms.
- Pint + PHPStan passed.
- Pest 256/256 за 27.75 сек (+8 от 248, 977 assertions).
Реестр v1.68→v1.69 / CLAUDE.md v1.59→v1.60.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 09:51:47 +03:00
Дмитрий
14dc317e2b
phase2(admin-incidents): GET /api/admin/incidents + AdminIncidentsView API (этап 4/5)
...
Чтение incidents_log с фильтрами type/severity/unresolved_only + summary
(open/investigating/rkn_pending/total_unresolved).
Backend (AdminIncidentsController::index):
- ORDER BY started_at DESC. Filters: type, severity, unresolved_only=true.
- Derived: incident_id (INC-YYYY-MMDD-NNNN), status (resolved_at!=null →
resolved; detected_at!=null → investigating; иначе open),
affected_tenants_count из BIGINT[] (parsePgArray для '{1,2,3}'),
rkn_deadline_at = detected_at+24h для data_breach без notification.
- summary: open/investigating/rkn_pending/total_unresolved.
Pest +11 (AdminIncidentsIndexTest):
- пустой / incident_id формат / derive status / filter type+severity /
unresolved_only / ORDER BY started_at DESC / rkn_deadline +24h для
data_breach / non-data_breach без deadline / summary.rkn_pending /
limit+offset.
- Quirk: saas_admin_users.full_name (не first/last) + нет updated_at.
Frontend:
- api/admin.ts::listAdminIncidents — типизированный helper.
- AdminIncidentsView: унифицированный IncidentRow (mock-category ↔
API-type, mock-title ↔ API-summary). Reactive rowsState+stats default
= MOCK; loadIncidents() async на onMounted; fetchError + warning
alert + MOCK fallback; reload-btn. РКН pending chip учитывает оба
pdn_breach/data_breach.
Vitest +5:
- listAdminIncidents на mount / replace state+stats + rkn_deadline /
reject → fetchError+alert+fallback / reload-btn x2 / РКН pending chip
виден для data_breach без notification.
PHPStan baseline регенерирован. cspell-glossary +MMDD.
Регресс:
- Lint+type-check+format passed.
- Vitest 305/305 за 20.59 сек (+5 от 300).
- Vite build 1.05 сек.
- Pint + PHPStan passed.
- Pest 248/248 за 28.02 сек (+11 от 237, 951 assertion).
Реестр v1.67→v1.68 / CLAUDE.md v1.58→v1.59.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 09:38:34 +03:00
Дмитрий
4532b95d64
phase2(admin-billing): GET /api/admin/billing + AdminBillingView API (этап 3/5)
...
Aggregates пополнений/списаний за текущий месяц по balance_transactions
+ summary с MRR/revenue/overdue/refunds_30d.
Backend (AdminBillingController::index):
- GET /api/admin/billing?search=. Per-tenant SUM с CASE WHEN type IN
('topup','lead_charge') GROUP BY tenant_id; ABS для charges.
- Row: id/subdomain/organization_name/contact_email/status/balance_rub/
tariff_id/tariff_name/mrr_rub (=tariff.price_monthly если не-trial)/
monthly_topups_rub/monthly_charges_rub/last_payment_at/
chargeback_unrecovered_rub.
- summary: total_mrr_rub (SUM не-trial), monthly_revenue_rub (SUM topup),
overdue_count (balance<0 || chargeback>0), refunds_count_30d.
- Quirk: schema-колонка tariff_plans.price_monthly (НЕ price_rub_monthly)
— обнаружено первым прогоном Pest, исправлено сразу.
Pest +9 (AdminBillingIndexTest):
- пустой / поля+tariff JOIN / aggregates за месяц / прошлый месяц не
попадает / overdue / refunds_30d (старые исключены) / total_mrr_rub
(trial исключаются) / search ILIKE / soft-deleted скрыт.
Frontend:
- api/admin.ts::listAdminBilling — типизированный helper.
- AdminBillingView: reactive rowsState+summary default = MOCK,
loadBilling() async на onMounted парсит API-строки → numbers + derive
status (suspended/balance<0||chargeback>0→overdue/active). На fail —
fetchError + warning alert + MOCK fallback. Reload-btn.
- tariffLabel/statusInfo обобщены с fallback'ами на новые slug'и.
Vitest +4:
- listAdminBilling на mount / replace rowsState+summary + string→number
+ status derive / reject → fetchError+alert+fallback / reload-btn x2.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 300/300 за 18.41 сек (+4 от 296).
- Vite build 925 ms.
- Pint + PHPStan passed.
- Pest 237/237 за 27.69 сек (+9 от 228, 926 assertions).
Реестр v1.66→v1.67 / CLAUDE.md v1.57→v1.58.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 09:28:49 +03:00
Дмитрий
6ef9961f5f
phase2(admin-tenants): GET /api/admin/tenants + AdminTenantsView API (этап 2/5)
...
AdminTenantsView переходит с mock-данных на live backend.
Backend (AdminTenantsController::index):
- GET /api/admin/tenants?status=&search=&limit=&offset=.
- LEFT JOIN tariff_plans для tariff_name. ORDER BY last_activity_at DESC.
- ILIKE search по organization_name + subdomain + contact_email.
- stats {total, active, trial, overdue} — overdue считает balance<0
ИЛИ chargeback_unrecovered_rub > 0.
- На MVP без auth (saas-admin SSO ⏸ Б-1).
Pest +8 (AdminTenantsIndexTest):
- 200 + пустой / все поля / status filter / search ILIKE /
ORDER BY last_activity_at DESC / stats / soft-deleted скрыт /
limit+offset.
Frontend:
- api/admin.ts::listAdminTenants — типизированный helper.
- composables/adminTenantsMapper.ts::mapApiAdminTenant — converter
API → UI: status derive (is_trial→trial, chargeback>0||balance<0
→overdue), inn='', code=subdomain, tariff clamp на known TenantTariff,
todayActual/mrrRub отсутствуют в API → 0/null, activitySince через
formatRelative.
- AdminTenantsView: reactive tenantsState+stats default = MOCK,
loadTenants() на onMounted → splice replace; на fail — fetchError +
warning alert + MOCK fallback. Reload-btn.
Vitest +13:
- View-integration (4): listAdminTenants на mount / replace state+stats /
reject → fetchError + alert + fallback / reload-btn x2.
- Mapper (9): name/code/inn/status-derives (trial/overdue/suspended) /
balance_rub→number / activitySince + null fallback.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 296/296 за 18.91 сек (+13 от 283).
- Vite build 1.02 сек.
- Pint + PHPStan passed.
- Pest 228/228 за 25.22 сек (+8 от 220, 906 assertions).
Реестр v1.65→v1.66 / CLAUDE.md v1.56→v1.57.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 09:19:53 +03:00
Дмитрий
e31ea5354a
phase2(lead-statuses): GET /api/lead-statuses + Pinia-store с snapshot fallback
...
Заменяет static-снапшот LEAD_STATUSES в коде на live-данные из БД.
Custom slug'и (добавленные после deployment'а) теперь видны UI без rebuild'а.
Backend:
- LeadStatus model (PK=slug string, incrementing=false, timestamps=null).
- LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order,
slug. Таблица глобальная (не tenant-aware), auth не требуется на MVP.
Pest +5 (LeadStatusesIndexTest):
- 200 + не пустой / все 14 системных slug'ов из seed / все нужные поля /
sort_order ASC / кастомный slug после INSERT появляется в endpoint'е.
Frontend:
- api/leadStatuses.ts::listLeadStatuses — GET helper.
- stores/leadStatuses.ts::useLeadStatusesStore — Pinia setup-store:
statuses default = LEAD_STATUSES snapshot (UI работает без fetch'а),
load(force=false) идемпотентен, bySlug computed Map, findBySlug helper.
На fail — snapshot остаётся, fetchError=true.
- DealsView/KanbanView/DealDetailDrawer переехали со static-импорта
LEAD_STATUSES на store. KanbanView использует safe-access
dealsByStatus[slug] || [] (защита от custom slug'а из API без seeded
column). load() в onMounted у обоих view'ов.
Vitest +7 (leadStatusesStore.spec.ts):
- initial snapshot / findBySlug existing & null / load success replace +
loaded / load reject — fetchError + snapshot fallback / load идемпотентен /
load(force=true) refetch.
- 2 spec'а DealDetailDrawer получили setActivePinia(createPinia()) в
beforeEach (без этого Pinia store-injection в jsdom падает).
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 280/280 за 19.44 сек (+7 от 273).
- Vite build 1.17 сек.
- Pint + PHPStan passed.
- Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions).
Реестр v1.63→v1.64 / CLAUDE.md v1.54→v1.55.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 08:59:17 +03:00
Дмитрий
cba76c5d18
phase2(deal-show): GET /api/deals/{id} + DealDetailDrawer на реальный ActivityLog
...
Закрывает gap «timeline в drawer'е показывает hard-coded MOCK_EVENTS» —
теперь drawer fetch'ит реальные activity-events на open из tenant-filtered
activity_log. Без tenant_id — fallback на MOCK_EVENTS как раньше.
Backend (DealController::show):
- GET /api/deals/{id}?tenant_id={id} — возвращает {deal, events}.
- Deal extended (project_name, manager_name/initials, comment, assigned_at).
- Events — последние 50 записей activity_log по (tenant_id, deal_id)
ORDER BY created_at DESC, с актором (user через belongsTo).
- RLS-обёртка + defense-in-depth where(tenant_id) — 404 если чужая.
Pest +8 (DealShowTest):
- 422/404 базовые / 404 чужая сделка / deal-relations / events ORDER BY +
actor + actor=null для system-event / RLS+app-фильтр изоляция событий /
лимит 50 событий.
Frontend:
- api/deals.ts::getDeal — типизированный helper c ApiDealEvent/Detail/Response.
- composables/dealsApiMapper.ts::mapApiDealEvent — converter ApiDealEvent →
DealEvent: clamp event-slug на known types с fallback на 'deal.viewed';
detail зависит от type (status_changed: 'from → to'; created: source;
остальные: JSON-сводка context).
- DealDetailDrawer: optional tenantId prop, watch([open, deal.id, tenantId])
с immediate=true → loadEvents() на open. Reject → eventsFetchError +
v-alert warning + MOCK_EVENTS fallback.
- DealsView/KanbanView передают :tenant-id="auth.user?.tenant_id".
Vitest +4 (DealDetailDrawerApi.spec.ts):
- Без tenantId — getDeal не вызывается + MOCK_EVENTS видны.
- С tenantId — getDeal + events замещены + 'new → paid' виден.
- reject → fetchError + alert + MOCK_EVENTS fallback.
- open=false → getDeal не вызывается.
PHPStan baseline регенерирован.
Регресс:
- Lint+type-check+format passed.
- Vitest 273/273 за 20.76 сек (+4 от 269).
- Vite build 1.12 сек.
- Pint + PHPStan passed.
- Pest 205/205 за 24.19 сек (+8 от 197, 812 assertions).
Реестр v1.62→v1.63 / CLAUDE.md v1.53→v1.54.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 08:21:50 +03:00
Дмитрий
a1ea003642
phase2(xlsx-export): PhpSpreadsheet 5.7 + format=csv|xlsx на /api/deals/export
...
Закрыт TODO «реальный XLSX-export» из v1.51. Russian users prefer .xlsx
(1С/Excel) — заменяет CSV как default. CSV остаётся через format=csv.
Backend (DealController::export):
- Body теперь: {tenant_id, ids, format?: 'csv' | 'xlsx'}; default 'csv'.
- buildXlsx: Spreadsheet + setTitle 'Сделки' + setCellValue A1..G1
headers + bold(A1:G1) + setAutoSize всех колонок A..G. Writer пишет
через ob_start/php://output для возврата бинарной строки.
- Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+ Content-Disposition с .xlsx.
Quirk: PhpSpreadsheet 5.x удалил deprecated setCellValueByColumnAndRow —
мигрировал на A1-нотацию (setCellValue('A2', $val)).
Pest +4 (DealCreateTest):
- xlsx binary с Content-Type + magic bytes "PK\x03\x04" + size >2KB.
- IOFactory::createReader('Xlsx') распаковывает: sheet «Сделки» +
A1='ID' bold + A2/B2/C2 — реальные данные сделки.
- 422 на неизвестный format.
- Default (без format) — backward-compat CSV.
Frontend:
- api/deals.ts разделён: exportDeals (CSV string) + exportDealsXlsx
(Blob, responseType='blob').
- applyBulkExport(format='xlsx' | 'csv') в DealsView — default 'xlsx'.
XLSX → triggerBlobDownload (новый helper). CSV → старый CSV-helper.
На fail — fallback на local CSV.
Vitest +3 (DealsListIntegration):
- xlsx default → exportDealsXlsx + Blob download + toast «XLSX».
- 'csv' → exportDeals + toast «CSV».
- xlsx reject → fallback на local CSV + toast «Backend недоступен».
PHPStan baseline регенерирован (удалена unmatched ignore-запись для
setCellValueByColumnAndRow). cspell-glossary +дефолтит +vnd +spreadsheetml.
Регресс:
- Lint+type-check+format passed.
- Vitest 269/269 за 18.49 сек (+3 от 266).
- Vite build 982 ms.
- Pint + PHPStan passed.
- Pest 197/197 за 26.05 сек (+4 от 193, 784 assertions).
Реестр v1.61→v1.62 / CLAUDE.md v1.52→v1.53.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 08:08:53 +03:00
Дмитрий
ac186593f2
phase2(bulk-transition+reload): POST /api/deals/transition + reload-btn
...
Закрывает gap «UI меняет статус, но изменения не сохраняются на backend»
из v1.51. Reload-btn заменяет polling/SSE до прихода long-poll'а на prod.
Backend (DealController::transition):
- POST /api/deals/transition {tenant_id, ids: [1..1000 ints], status}.
- Валидация status — exists в lead_statuses (глобальная таблица).
- RLS-обёртка SET LOCAL + defense-in-depth where(tenant_id) для
partial-update: чужие id остаются в исходном статусе.
- ActivityLog event=deal.status_changed с context={from, to, source: 'bulk'}
для каждой ИЗМЕНЁННОЙ сделки. NO-OP (старый==новый) не пишется в audit.
- Ответ: {updated, requested, status}.
Pest +7 (DealTransitionTest):
- 422 missing fields / 404 unknown tenant / 422 неизвестный slug + не апдейт /
batch update 3 сделок + 3 ActivityLog с правильным context /
NO-OP не пишет ActivityLog / defense-in-depth (2 tenant'а — обновляется
только свой) / 422 пустой массив ids.
Frontend:
- dealsApi.transitionDeals — типизированный helper с ensureCsrfCookie.
- applyBulkStatus в DealsView переписан async: optimistic local-update +
backend-вызов если auth.user.tenant_id. На success — toast «Обновлено
N из M.», на fail — warning toast + локальный update НЕ откатывается.
Без auth.user — только optimistic (legacy local-mode сохранён).
- reload-btn в DealsView и KanbanView — outlined «Обновить» mdi-refresh,
привязан к loadDeals. В DealsView :loading="loading" во время fetch'а.
Vitest +5:
- reload-btn (Deals + Kanban) — listDeals вызывается дважды.
- applyBulkStatus с tenant_id — transitionDeals + optimistic + toast.
- applyBulkStatus без tenant_id — НЕ вызывается transitionDeals.
- applyBulkStatus reject — toast warning + локальный update остаётся.
PHPStan baseline регенерирован. cspell-glossary +апдейт*.
Регресс:
- Lint+type-check+format passed.
- Vitest 266/266 за 18.16 сек (+5 от 261).
- Vite build 1.06 сек.
- Pint + PHPStan passed.
- Pest 193/193 за 23.27 сек (+7 от 186, 767 assertions).
Реестр v1.60→v1.61 / CLAUDE.md v1.51→v1.52.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 07:46:19 +03:00
Дмитрий
339e8ea53b
phase2(deals-list-api): GET /api/deals + замена MOCK_DEALS на fetch с fallback
...
Закрыт TODO (c) из v1.50: backend-эндпоинт для списка сделок и его
интеграция в DealsView/KanbanView вместо статичного MOCK_DEALS.
Backend (DealController::index):
- Query-params: tenant_id (required, 422/404), status_in[] (whereIn),
project_id, manager_id, search (ILIKE по phone+contact_name),
limit clamp [1..500] default 100, offset default 0.
- ORDER BY received_at DESC, id DESC. Eager-load project + manager.
- RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth
where(tenant_id) — на тестах через postgres superuser RLS обходится
BYPASSRLS, app-фильтр гарантирует изоляцию.
- Ответ: {deals: [...], total, limit, offset}; manager_name/initials
форматируются через ManagerController::formatName/formatInitials.
Pest +12 (DealIndexTest):
- 422/404, пустой список, relations (project_name+manager_name+initials),
RLS-изоляция, ORDER BY, status_in[], project_id, manager_id, search
ILIKE, limit+offset, manager=null edge case.
Frontend:
- api/deals.ts::listDeals — типизированный helper c ApiDeal/ListDeals*.
- composables/dealsApiMapper.ts::mapApiDeal — converter ApiDeal→MockDeal:
contact_name fallback на phone, manager.name='Не назначен' /
initials='—' при null, project='—' при null, cost=0,
receivedMinutesAgo=max(0, …) от clock-skew.
- DealsView/KanbanView: onMounted(loadDeals) async-вызывает listDeals
если auth.user.tenant_id, на success replace через splice, на fail
fetchError=true + v-alert warning, MOCK_DEALS как fallback.
Vitest +14:
- dealsApiMapper.spec.ts (8): 1:1, fallback'и, edge cases.
- DealsListIntegration.spec.ts (6): без tenant_id — НЕ вызывает API,
с tenant_id — replace state, reject → fetchError + alert + fallback;
для DealsView и KanbanView.
PHPStan baseline регенерирован. cspell-glossary +ILIKE +DTO.
Регресс:
- Lint+type-check+format passed.
- Vitest 261/261 за 19.62 сек (+14 от 247).
- Vite build 989 ms.
- Pint + PHPStan passed.
- Pest 186/186 за 22 сек (+12 от 174, 742 assertions).
Реестр v1.59→v1.60 / CLAUDE.md v1.50→v1.51.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 07:34:39 +03:00
Дмитрий
83bb9de2bb
phase2(backend-completion): POST /api/deals + webhook_hmac_required + POST /api/deals/export
...
3 backend-completion после tightening v1.56.
(1) POST /api/deals — manual create endpoint:
- DealController::store. Project firstOrCreate (type='manual'). Deal с
source_crm_id=NULL. RLS-обёрнутая транзакция.
- Manual НЕ списывает баланс / НЕ дедуп / НЕ SupplierLeadCost.
ActivityLog с context.source=manual.
- NewDealDialog получил optional tenantId prop. С tenantId — POST → backend-id;
на error fallback на local-id + warning + dialog open.
- DealsView/KanbanView передают auth.user?.tenant_id.
- Pest +8.
(2) webhook_hmac_required flag в system_settings:
- Seed-row в db/schema.sql (default false backward-compat).
- WebhookReceiveController::isHmacRequired private helper.
- При true: запрос без X-Webhook-Signature → 401.
- Pest +3.
(3) POST /api/deals/export — backend CSV:
- DealController::export. Валидация ids[1-10000]. RLS-обёрнутый whereIn.
- Excel-friendly CSV: BOM "\u{FEFF}" PHP-литерал, ; разделитель, \r\n.
- text/csv attachment headers.
- Frontend applyBulkExport: backend → fallback на client-side
(buildLocalCsv вынесен).
- Pest +4.
Vitest +3 (всего 245/245).
PHPStan убрал лишнюю Deal->id===null проверку (Eloquent int).
DealsView/KanbanView spec'ы получили setActivePinia.
Регресс: lint+type-check+format ✅ ; vitest 245/245 за 17.07 сек (+3);
vite build 1.04 сек; Pint+PHPStan passed; Pest 156/156 за 20.27 сек
(+15 от 141, 675 assertions). Реестр v1.56→v1.57, CLAUDE.md v1.47→v1.48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 06:43:21 +03:00