Дмитрий
546ca30a7e
fix(billing-v2): supplier_lead_deliveries migration prod-compatible — pgsql_supplier connection + explicit GRANTs + drop-index no-op
2026-05-24 06:43:40 +03:00
Дмитрий
84dbfb8691
chore(billing-v2): drop unused deals(duplicate_of_id) index (Spec B)
2026-05-23 20:53:51 +03:00
Дмитрий
bc8afbc362
feat(billing-v2): supplier_lead_deliveries lock table (Spec B)
...
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-23 20:44:52 +03:00
Дмитрий
d726d92427
refactor(billing-v2): seeders/factories — drop prepaid balance_leads defaults
2026-05-23 18:46:17 +03:00
Дмитрий
e3dc28d0bd
feat(billing-v2): add BalanceTransaction::TYPE_MIGRATION + extend CHECK
...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-23 18:46:08 +03:00
Дмитрий
60ab5be3eb
feat(audit): partitioning 7 audit-таблиц по месяцам (hole #2 Phase A)
...
Закрывает последнюю дыру #2 аудита журналирования. Phase A (dev) — миграция
схемы + retention tooling. Phase B (прод-rewrite через SQL под postgres) —
отдельным шагом с явным approve.
Решения заказчика:
* Scope: все 7 таблиц (auth_log, activity_log, tenant_operations_log,
webhook_log, balance_transactions, pd_processing_log, saas_admin_audit_log)
* FK на webhook_log: W1 — удалить FK от failed_webhook_jobs+rejected_deals_log
* Retention defaults: auth:24м, activity:36м, tenant_ops:24м, webhook:3м,
balance:84м, pd:36м, saas_admin:84м. Cron Sundays 03:00 МСК
* Hash-chain: per-partition (audit_chain_hash трг через TG_TABLE_NAME уже
работает per-partition; совместимо с hole #1 per-RLS-scope fix)
Phase A:
* db/schema.sql v8.30→v8.31: 7 audit-таблиц на PARTITION BY RANGE,
PK→(id, partition_key), +7 retention seeds в system_settings,
FK от failed_webhook_jobs/rejected_deals_log удалены
* MonthlyPartitionManager: PARTITIONED_TABLES → ассоциативный array
(name => partition_key), 2 → 9 таблиц
* PartitionsCreateMonths: автоматически покрывает все 9
* load_initial_schema: после schema.sql вызывает Artisan
partitions:create-months --ahead=2 (без этого первый INSERT падает)
* 2026_05_22_000001_tenant_operations_log: idempotency guard
* VerifyAuditChains: per-partition scan через pg_inherits;
fallback на single-scope для не-партиционированной таблицы;
per-RLS-scope partition_clause сохранён внутри каждой партиции
* AuditChainBreachMail: +partitionName param (NULL=fallback на tableName)
* PartitionsDropExpired (новая): cron Sundays 03:00 МСК, retention из
system_settings, dry-run mode, safety guard retention=0
* SchedulerHeartbeatTracker +partitions:drop-expired (10080 мин)
Без Laravel-миграции для прода — она оставляла БД пустой при migrate:fresh.
Подход: schema.sql декларирует партиционированные + ad-hoc SQL под postgres
для прод-rewrite (отдельный commit + ручной деплой + pg_dump backup).
Тесты: 1219/1231 (35/35 hole #2 specs, 88 assertions). 3 fail —
pre-existing AdminPdSubjectRequestsControllerTest::executeErasure_*
(FK actor_admin_user_id после partitioning pd_processing_log, отдельная
задача для hole #4 follow-up, не блокирует).
cspell +2 слова (партиционировать, дёшева). Pint --fix чистый.
Spec: docs/superpowers/specs/2026-05-23-hole-2-audit-partitioning-design.md
Plan: docs/superpowers/plans/2026-05-23-hole-2-audit-partitioning-plan.md
2026-05-23 15:50:37 +03:00
Дмитрий
c76038d076
feat(ops): scheduler heartbeat — пульс 11 cron-задач + watcher (hole #6 )
...
Закрывает дыру #6 из аудита журналирования 23.05.2026.
Что:
* `scheduler_heartbeats` таблица (SaaS-level, PK=command_name, без RLS)
* `SchedulerHeartbeatTracker` сервис — UPSERT через pgsql_supplier (BYPASSRLS),
recordRun(callable) + recordRunResult(name, success, error, ms)
* `routes/console.php` — 11 cron-задач обёрнуты onSuccess/onFailure хуками
(минимально-инвазивно, без правки самих джобов)
* `scheduler:check-heartbeats` команда — hourly МСК:
- алертит при пропавшем пульсе (>2× ожидаемого интервала)
- алертит при consecutive_failures >= 3
- dedup 60 мин, пишет incidents_log (severity=high) + Mail на kdv1@bk.ru
* `SchedulerHeartbeatMissingMail` mailable + blade
NB: используется `onSuccess()` а не `after()` — `after()` срабатывает при любом
исходе и ложно обновлял бы last_success_at при failure (правильный поведенческий
паттерн = onSuccess + onFailure). consecutive_failures корректно растёт через
ON CONFLICT DO UPDATE +1.
Schema bump v8.29→v8.30. +1 слово в cspell-words.txt (FQCN).
Тесты: 8/8 passed (24 assertions, ~1.6s) — recordRun success/failure,
SchedulerCheckHeartbeats missing pulse + failure spike + dedup + Mailable.
Plan: docs/superpowers/plans/2026-05-23-7-holes-overview.md (#6 ).
2026-05-23 11:48:20 +03:00
Дмитрий
5df34a61eb
style+done(p2): pint formatting + P2 plan DONE marker
2026-05-22 18:53:11 +03:00
Дмитрий
57d84c6ea3
feat(audit): Task 7 — log all SupplierWebhookController outcomes to webhook_log
...
- schema v8.29: webhook_log +source/status/lead_id/ip_address/created_at,
tenant_id nullable, +idx_webhook_log_status
- migration 2026_05_22_000002_webhook_log_supplier_columns
- SupplierWebhookController::logSupplierWebhook() private helper (silent/non-throwing)
called at 4 exit points: rejected_secret/rejected_ip/rate_limited/received
- SupplierWebhookLoggingTest: 4 tests 17 assertions GREEN
- Regression SupplierWebhookTest: 13/13 GREEN
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-22 18:53:10 +03:00
Дмитрий
948bdb72d1
feat(schema): tenant_operations_log table with hash-chain protection (P2)
...
+1 table tenant_operations_log — журнал тенант-уровневых операций вне сделок
(проекты, API-ключи, webhook URL). Параллельна activity_log без deal_id NOT NULL.
- Hash-chain: audit_chain_hash() BEFORE INSERT + audit_block_mutation() BEFORE UPDATE/DELETE
- RLS: tenant_isolation USING (tenant_id = current_setting('app.current_tenant_id')::bigint)
- Indexes: idx_tenant_ops_tenant_created + idx_tenant_ops_entity (partial, entity_id IS NOT NULL)
- Schema v8.28: 66 tables (64 regular) / 125 indexes / 41 RLS / 15 triggers
- Applied: liderra ✅ + liderra_testing ✅
- Smoke: INSERT hash_len=32 ✅ / UPDATE blocked (audit log is append-only) ✅
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-22 18:53:06 +03:00
Дмитрий
07d73870ba
refactor(projects): remove archive feature, drop archived_at column (schema v8.27)
2026-05-21 08:24:25 +03:00
Дмитрий
e6d6babb38
feat(supplier): deals.subject_code range CHECK 1..89 (defensive parity)
2026-05-20 11:15:14 +03:00
Дмитрий
c5ec9a0875
feat(supplier): backfill project_supplier_links from legacy FK slots
2026-05-20 11:06:13 +03:00
Дмитрий
1ba8b6e590
feat(supplier): seed supplier_export_mode toggle (v8.26)
2026-05-20 10:59:27 +03:00
Дмитрий
148262a78e
feat(supplier): deals.subject_code from supplier tag (v8.26)
2026-05-20 10:57:04 +03:00
Дмитрий
787c38ad82
feat(supplier): project_supplier_links M:N pivot (v8.26)
2026-05-20 10:54:50 +03:00
Дмитрий
82c0aeef41
feat(supplier): supplier_projects.subject_code + per-subject unique index (v8.26)
2026-05-20 10:45:02 +03:00
Дмитрий
fdd8247527
fix(tests): ProjectFactory unique name — Str::random suffix (quirk #77 )
...
fake()->unique() builds a fresh UniqueGenerator per definition() call, so
uniqueness is not guaranteed within a batch — names collided on the
(tenant_id, name) UNIQUE under pest --parallel. Append Str::random(8)
(62^8 ≈ 2e14 space) to eliminate the collision.
Verified: ProjectBulkActions 15/15 ×2 parallel runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-20 10:28:32 +03:00
Дмитрий
d369383c7d
feat(supplier): supplier_manual_sync_queue table (Tier 3 queue)
...
SaaS-level (без tenant_id, без RLS, как supplier_csv_reconcile_log).
+3 CHECK (platform/operation/status), +2 индекса, +2 FK
(project_id→projects CASCADE, resolved_by_user_id→users SET NULL).
Миграция через DB::unprepared (PG prepared statement не разрешает multi-SQL).
schema.sql bumped v8.24 → v8.25 (64 base tables / 121 indexes / 40 RLS).
SchemaDeltaTest обновлён под новые метрики (63→64 tables, 119→121 indexes).
§15.2 pre-flight: rebase на origin/main f7f37fb выполнен до коммита.
Spec §4.5. Task 3 of 12. Регрессия: schema+delta тесты 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-19 12:55:06 +03:00
Дмитрий
ed8ec89bcc
feat(supplier): supplier_leads.vid -> nullable для CSV-recovered лидов
...
Резервный CSV-канал (Путь 2): отчёт поставщика «Запрос номеров» не
содержит vid -> CSV-recovered лиды имеют vid=NULL. UNIQUE-индекс
idx_supplier_leads_vid_unique сохранён (PostgreSQL NULL != NULL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-18 17:20:28 +03:00
Дмитрий
77cc535ab2
feat(deals): migration — remap deals.status + drop obsolete lead_statuses (14->5)
2026-05-18 03:42:41 +03:00
Дмитрий
ec6ebc57e0
merge: C9 — Plan 6 регионы субъект-уровня в портал
...
# Conflicts:
# app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
# db/CHANGELOG_schema.md
# db/schema.sql
2026-05-17 09:30:21 +03:00
Дмитрий
72c8cad963
fix(dev): A8 review — production-guard в DemoSeeder + точность README/теста (Sprint 5A)
2026-05-17 02:23:26 +03:00
Дмитрий
8bc8c53a3b
feat(import): Eloquent-модели ImportLog + ImportUnknownStatus
...
- ImportLog: $attributes зеркалят DB DEFAULT'ов (status/entity_type/dry_run),
CREATED_AT/UPDATED_AT=null (таблица использует started_at/finished_at),
casts для mapping_config (array) и dry_run (boolean)
- ImportUnknownStatus: scope unresolved() (whereNull mapped_to_slug),
BelongsTo tenant
- Фабрики ImportLogFactory + ImportUnknownStatusFactory
- Тест ImportModelsTest (2/2, DatabaseTransactions, idempotent)
- ide-helper:models перегенерирован под новые модели
- phpstan-baseline регенерирован (квирк 25: TestCall::$tenant/$user)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 17:29:33 +03:00
Дмитрий
70f8b210f4
feat(import): H1+H2 — схема import_unknown_statuses + enrichment import_log
...
Sprint 4 Task 1 (schema delta §6):
- H1: новая таблица import_unknown_statuses (RLS tenant_isolation,
UNIQUE(tenant_id,status_ru), FK→tenants/import_log/lead_statuses/users)
- H2: +5 колонок import_log (entity_type, source_system, mapping_config,
unknown_statuses_count, dry_run)
- schema.sql v8.20→v8.21 (64 таблицы / 118 индексов / 40 RLS-политик)
- db/CHANGELOG_schema.md v8.21 entry
- db/02_grants.sql v8.21 section (crm_app_user/crm_app_admin/crm_readonly)
- migrate: hasTable/hasColumn guards (fresh-safe)
- tests: 3 Pest-теста (ImportSchemaTest) + SchemaDeltaTest v8.21 metrics
- ide-helper: _ide_helper.php + _ide_helper_models.php (были отсутствуют
в worktree, phpstan падал молча из-за missing scanFiles entry)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 17:01:51 +03:00
Дмитрий
040d25423d
feat(billing): wallet/transactions/invoices read API (E3)
...
GET /api/billing/wallet (баланс + тариф + runway), /transactions
(пагинированный balance_transactions с фильтром type), /invoices
(saas_invoices, real-but-empty до Б-1). TariffPlan модель +
Tenant::tariff() relation + BalanceTransactionFactory.
Sprint 2 Plan C, audit E3 (backend).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 07:08:09 +03:00
Дмитрий
dc9cab300c
test(api): WebhookSettings — tenant-isolation + failure-path coverage (review M2/M3/M4)
...
Code-quality review of Task 4: adds a cross-tenant isolation test
(verifies the where(tenant_id) guard, matching ApiKeyControllerTest)
and a test()-endpoint failure-path test (HTTP 500 -> ok=false). Drops
the @return docblock from OutboundWebhookSubscriptionFactory for
consistency with ApiKeyFactory, eliminating a baseline entry at source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-15 22:21:52 +03:00
Дмитрий
3266909346
feat(api): outbound webhook settings endpoints (closes J5 part 2)
...
Audit J5/D4/D5: the outbound_webhook_subscriptions table existed in
schema but had zero code. Adds the OutboundWebhookSubscription model +
factory and WebhookSettingsController with GET/PUT
/api/tenants/me/webhook-settings (one subscription per tenant; secret
generated + returned once on creation, bcrypt-hashed) and POST
/api/webhooks/test (unsigned connectivity check — HMAC-signed event
delivery is a separate post-MVP epic). Tenant-scoped via auth:sanctum +
tenant middleware.
phpstan-baseline.neon: additive-only entries for new test file
(Pest\PendingCalls\TestCall false-positives — documented project pattern)
and OutboundWebhookSubscriptionFactory method.childReturnType (same
pattern as ProjectFactory/TenantFactory/UserFactory already in baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-15 22:13:32 +03:00
Дмитрий
a5e2bbbbe8
feat(api): api_keys model + GET/regenerate endpoints (closes J5 part 1)
...
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 >
2026-05-15 21:53:35 +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
Дмитрий
219f262655
fix(test): ProjectFactory unique name + test:parallel composer alias
...
fake()->unique()->words(3,true) fixes quirk #77 deterministic collision
on projects(tenant_id,name) UNIQUE in --parallel runs.
test:parallel alias = pest --parallel --recreate-databases (quirk #62/#73).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-13 13:32:00 +03:00
Дмитрий
1da23b8253
chore(audit): finalize 2026-05-12 portal full audit
...
Полный аудит портала проведён в ночь 12.05.2026 на ветке plan5-frontend-projects.
9 phase'ов, 393 findings, 8 fix-commits, 4 BLOCKED-вопроса.
Артефакты:
- docs/superpowers/plans/2026-05-12-portal-full-audit.md — план
- docs/superpowers/audits/*-findings.md — все findings file:line + severity
- docs/superpowers/audits/*-blocked.md — 4 вопроса заказчику
- docs/superpowers/audits/*-report.md — summary с метриками до/после
- audit-screens/views/ — 24 UI smoke screenshots (Playwright)
- audit-screens/legacy/ — 32 untracked PNG из workdir
- app/database/seeders/DemoSeeder.php — idempotent seed
- .gitleaks.toml — allowlist для seeders/audit-docs (демо-фикстуры)
- cspell-words.txt — +12 audit-cited mixed-script artifacts
Метрики (Phase 1+2 baseline → Phase 9 final, все commits 3a8229a..57f0b8e):
- Histoire build BROKEN → 35 stories / 63 variants ✅
- ESLint 17 → 0 ✅
- vue-tsc 9 → 0 ✅
- Prettier 48 → 0 ✅
- markdownlint 165 → 1 (untracked design.md) ✅
- cspell 103 → 18 → 0 (after audit-cited words added) ✅
- Vitest 614 → 614 (0 regression) ✅
- Pest --parallel 739/0/3 → 739/0/3 ✅
- Vite build 1.80s 0 warnings → 1.72s 0 warnings ✅
- gitleaks 0 leaks (340 commits) ✅
🟢 GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 20:37:51 +03:00
Дмитрий
2ffbb49faa
fix(projects): Plan 5 Task 3 code-review fixes (2 Important + 2 Minor)
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 18:38:52 +03:00
Дмитрий
9d2e7270de
feat(projects): Plan 5 Task 3 — store + StoreProjectRequest + ProjectService::create
...
- StoreProjectRequest: 3-way conditional validation (site domain regex, call 7\d{10}, sms senders required)
- ProjectService::create(): max_projects limit check via Tenant.limits JSONB + dispatch SyncSupplierProjectJob
- ProjectController: constructor DI + store() method returning 201
- SyncSupplierProjectJob: stub (Task 4 полная реализация)
- POST /api/projects route inside auth:sanctum+tenant group (name projects.store)
- Migration add_limits_to_tenants: JSONB DEFAULT '{}' per-tenant limits column
- Tenant model: limits added to fillable + casts as array
- schema.sql/CHANGELOG: tenants.limits documented in v8.20
- phpstan-baseline: +8 actingAs entries for new test file
- Quirk: region_mode in request uses 'include'/'exclude' (schema CHECK) not 'all'/'whitelist' (plan spec typo)
- Quirk: Project::first() → Project::where('signal_identifier','x.ru')->latest()->first() (no RefreshDatabase, persistent test DB)
- 8/8 ProjectsStoreTest passed; 699/706 total (4 pre-existing failures unchanged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 18:29:54 +03:00
Дмитрий
622773f929
fix(db): Plan 5 Task 1 code-review fixes (2 Important + 2 Minor)
...
I-1: scopeActive docblock — явное предупреждение что scope НЕ фильтрует
is_active; приостановленные проекты попадают; пример комбинирования.
I-2: migration down() — комментарий об асимметрии с up() и риске drift
с schema.sql v8.20 при случайном rollback.
M-1: archived_at перемещён в $fillable на позицию сразу после is_active
(lifecycle-state рядом с lifecycle-state, как указано в плане).
M-2: CHANGELOG header счётчик восемнадцать → девятнадцать записей.
Tests: ArchivedAtTest 2/2 PASS (4 assertions, 472 ms). No behavior change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 17:51:33 +03:00
Дмитрий
144d4cbb98
feat(db): Plan 5 Task 1 — schema delta v8.19 → v8.20 + Project.archived_at
...
Schema delta (1 правка в db/schema.sql):
- projects + archived_at TIMESTAMPTZ NULL — soft archive flow (отличие от
is_active=false который = pause).
Метрики: 62 базовых таблицы / 117 индексов / 39 RLS (без изменений).
Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.20 entry.
- app/Models/Project — fillable+casts: archived_at datetime + scopeActive +
scopeArchived (whereNull/whereNotNull archived_at).
- Migration guard: Schema::hasColumn() проверка перед ALTER TABLE — предотвращает
"duplicate column" после migrate:fresh (schema.sql v8.20 уже содержит колонку).
Tests:
- ArchivedAtTest.php — 2 it() блоков: archived_at колонка timestamptz + fillable/casts.
- pest --filter=ArchivedAtTest: 2/2 PASS (4 assertions, 485 ms).
- Full suite: 689/686+3 skipped/0 failed (2094 assertions, 84638 ms).
Quirk зафиксирован: Schema::getColumnType('projects', 'archived_at') → 'timestamptz'
(не 'timestamp') — PostgreSQL TIMESTAMPTZ → Doctrine/Laravel native type string.
План spec ожидал 'timestamp', скорректировано в тесте с комментарием.
Spec: docs/superpowers/specs/2026-05-10-claude-brain-extraction-design.md (Plan 5).
Plan: docs/superpowers/plans/2026-05-10-claude-brain-extraction.md Task 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 17:39:46 +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
Дмитрий
a907fea031
feat(db): Plan 4 Task 1 — schema delta v8.18 → v8.19 + models/seeder
...
Schema delta (4 правки в db/schema.sql):
- tenants + delivered_in_month INT NOT NULL DEFAULT 0 CHECK (>=0) — month
counter для PricingTierResolver O(1) lookup на горячем пути.
- lead_charges + charge_source VARCHAR(8) DEFAULT 'rub' CHECK IN ('prepaid','rub')
+ ALTER ADD CONSTRAINT chk_lead_charges_prepaid_zero_price (prepaid → price=0).
- supplier_leads + recovered_from_csv_at TIMESTAMPTZ + partial index
WHERE recovered_from_csv_at IS NOT NULL.
- Новая таблица supplier_csv_reconcile_log (SaaS-level, без RLS) + 2 индекса
(started_at DESC, partial status WHERE status IN ('drift_alert','failed')).
Метрики: 61 → 62 базовых таблиц / 114 → 117 индексов / 39 RLS (без изменений).
Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.19 entry (18 записей).
- db/02_grants.sql — GRANT SELECT,INSERT,UPDATE on supplier_csv_reconcile_log
+ GRANT USAGE,SELECT on sequence для crm_supplier_worker.
- app/Models/Tenant — fillable+casts: delivered_in_month integer.
- app/Models/LeadCharge — fillable+casts: charge_source string.
- app/Models/SupplierLead — fillable+casts: recovered_from_csv_at datetime.
- LeadChargeFactory — defaults charge_source='rub' + prepaid() state.
- PricingTierSeeder (новый) — 7 ступеней дефолтного тарифа (placeholder,
Plan 4 Открытый вопрос #1 : 100/200/400/800/1500/3000/∞ leads at
50000/45000/40000/35000/30000/27000/25000 копеек).
- DatabaseSeeder — call PricingTierSeeder; убран broken Laravel scaffold
User::factory(['name' => ...]) (наша схема first_name/last_name).
Tests (Plan 4 surface):
- SchemaDeltaTest.php — 5 it() блоков: delivered_in_month CHECK, charge_source
CHECK на prepaid+zero-price, recovered_from_csv_at колонка, reconcile_log
таблица+status CHECK, migrate:fresh idempotency (skip in parallel).
- pest --filter=SchemaDeltaTest: 5/5 PASS (9 assertions, 2076 ms).
- pest --filter='Tenant|LeadCharge|SupplierLead|Plan4': 111/111 PASS.
CI gates:
- composer pint: passed.
- composer stan: passed (0 errors above baseline — @phpstan-ignore-next-line
на $this->markTestSkipped в Pest closure rebound context).
Verify:
- migrate:fresh --seed на DB_DATABASE=liderra_testing: 0 errors, 754 ms.
- PricingTier::count() = 7.
Концерны (НЕ блокируют Task 1):
- pest --parallel: 617/622 PASS + 4 skipped + 1 flaky FAIL — flaky test
колеблется между ProjectExtensionsTest::supplierB1_B2_B3_relations
(SupplierProjectFactory race: closure пикает signal_type=sms до override
platform=B1 → CHECK chk_supplier_projects_b1_not_for_sms violation)
и NewLeadNotificationTest::webhook_дубль_Биз_19 (известный microsecond
precision quirk в anti-spam exclusion, memory feedback_environment.md).
Оба теста существуют на main (HEAD 0802f7c = plan4-billing branch HEAD
до этого commit'а), Plan 4 их не трогает. Фиксы — вне Task 1 scope.
Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §2.
Plan: docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin-plan.md Task 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 08:38:38 +03:00
Дмитрий
aa37f4cbed
feat(models): SupplierLead model + factory (raw-payload incoming webhooks)
...
SaaS-level модель для supplier_leads (Plan 2/5 Task 2).
belongsTo(SupplierProject) + array cast на raw_payload + datetime *_at.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 18:24:45 +03:00
Дмитрий
99afcbc25c
feat(models): extend Project with signal_type, sms_senders, supplier_b1/b2/b3 relations + scopes
...
- app/Models/Project.php — добавлены fillable+casts для supplier integration:
signal_type, signal_identifier, sms_senders (jsonb array), sms_keyword,
delivered_in_month, supplier_b{1,2,3}_project_id.
+ supplierB1/B2/B3() BelongsTo relations на SupplierProject (sharing-model).
+ scopeActiveOnDay($iso) — bitmask проверка по delivery_days_mask
(bit 0 = Mon, bit 6 = Sun; ISO=1 → 1<<0 = 1; ISO=7 → 1<<6 = 64).
+ scopeForSignal($type, $identifier) — фильтр по сигналу (для роутинга в Plan 2).
- database/factories/ProjectFactory.php — defaults null/0 для новых полей
(CHECK constraints не нарушаются: signal_type IS NULL → остальные опциональны).
+ state-методы asSiteSignal($domain), asCallSignal($phone), asSmsSignal($senders, $keyword).
- tests/Feature/Models/ProjectExtensionsTest.php — 6 тестов: signal_type fillable,
sms_senders array cast + sms_keyword, SMS без keyword, supplierB1/B2/B3 relations,
scopeActiveOnDay (bitmask Mon/Sat), scopeForSignal (3 сигнала + edge-case).
Pest: 469 / 467 passed / 2 skipped (461 + 6 новых = 467, с retry на transient
PG connection issues — на параллельных тестах с testing_rls_user GRANT тяжёл).
Larastan: 0 errors. Pint passed.
Spec: §2.1
Plan: Task 10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 16:59:53 +03:00
Дмитрий
084a952bfa
feat(models): add SupplierSyncLog model + factory (audit trail для AJAX-sync)
...
- app/Models/SupplierSyncLog.php — fillable + casts (jsonb arrays + datetime)
+ supplierProject() BelongsTo relation (nullable, ON DELETE SET NULL —
лог переживает удаление supplier-проекта для audit-trail).
$timestamps = false (только created_at, без updated_at — append-only)
- database/factories/SupplierSyncLogFactory.php — реалистичные действия из enum
- tests/Feature/Models/SupplierSyncLogTest.php — 4 теста: factory,
supplier_project relation, jsonb array casts, nullable FK lifecycle
Pest: 463 / 461 passed / 2 skipped (457 + 4 новых = 461).
Larastan: 0 errors. Pint passed.
Spec: §4.3
Plan: Task 9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 16:53:55 +03:00
Дмитрий
31f813315d
feat(models): add LeadCharge ledger model + factory + relations to Tenant/Deal
...
- app/Models/LeadCharge.php — fillable + casts (datetime + integer)
+ tenant() BelongsTo relation
+ deal() BelongsTo relation (по deal_id, без deal_received_at — composite PK
на партиционированной обеспечивается БД-уровнем через FK)
+ accessor priceRubles (kopecks → float)
- database/factories/LeadChargeFactory.php — НЕ создаёт реальный Deal автоматически
(composite FK requires (deal_id, deal_received_at) пары); тесты с FK-целостностью
явно создают Deal::factory() и передают пару в state()
- tests/Feature/Models/LeadChargeTest.php — 4 теста: factory, tenant relation,
deal relation, priceRubles accessor. testing_rls_user setup в beforeEach
для проверки RLS context из не-superuser контекста.
Quirk: SET LOCAL app.current_tenant_id НЕ принимает параметрическое связывание PG —
используем string interpolation с {$tenant->id} как в RlsSmokeTest pattern.
ide-helper:models -W -M -N синхронизировал docblocks (WebhookDedupKey).
Pest: 459 / 457 passed / 2 skipped (453 + 4 новых = 457).
Larastan: 0 errors. Pint passed.
Spec: §7.4
Plan: Task 8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 16:52:10 +03:00
Дмитрий
71e38ee0a9
feat(models): add PricingTier model with kopecks→rubles accessor + current() snapshot
...
- app/Models/PricingTier.php — fillable + casts (date, boolean, integer)
+ accessor priceRubles (kopecks → float rubles)
+ scopeActive (is_active=true AND effective_from <= today)
+ static current() — keyed by tier_no Collection<int, PricingTier>
- database/factories/PricingTierFactory.php — реалистичные ступени (300/700/1000/.../null)
- tests/Feature/Models/PricingTierTest.php — 4 теста: factory, accessor,
scopeActive, current() snapshot всех 7 ступеней
ide-helper:models -W -M -N перегенерил docblocks (WebhookDedupKey synced
после schema v8.16).
Pest: 455 / 453 passed / 2 skipped (449 + 4 новых = 453).
Larastan: 0 errors. Pint auto-fix.
Spec: §7.2
Plan: Task 7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 16:48:00 +03:00
Дмитрий
62aa55f033
feat(models): add SupplierProject Eloquent model + factory + tests
...
- app/Models/SupplierProject.php — fillable + casts (jsonb arrays + datetime)
+ scopes: active() (inactive_since IS NULL), staleSince(N days),
forSignal(signal_type, unique_key)
- database/factories/SupplierProjectFactory.php — корректно учитывает
chk_supplier_projects_b1_not_for_sms (B1 не порождает SMS-проекты)
- tests/Feature/Models/SupplierProjectTest.php — 6 тестов: factory,
array casts (workdays + regions), scopeActive, scopeStaleSince,
scopeForSignal (3 платформы на один домен — UNIQUE (platform,unique_key))
ide-helper:models -W -M -N перегенерил docblocks для 4 существующих моделей
(SaasAdminAuditLog, SystemSetting, UserRecoveryCode, ImpersonationToken) —
синхронизировал @property после schema v8.16.
Pest: 451 / 449 passed / 2 skipped (было 443+6 новых от Task 6 = 449).
Larastan: 0 errors. Pint: passed.
Spec: §2.2
Plan: Task 6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 13:59:39 +03:00
Дмитрий
757f963929
fix(db): consolidate Plan 1 Tasks 1-5 into schema.sql (project convention)
...
Project convention использует schema.sql как single source of truth + один
load_initial_schema migration вместо incremental migrations. Мои 5 incremental
миграций конфликтовали с migrate:fresh: load_initial_schema применял
обновлённый schema.sql v8.16, а потом 000001-000005 пытались добавить уже
существующие колонки/таблицы (`signal_type already exists`).
Изменения:
- Удалены 5 incremental миграций 2026_05_10_00000{1..5}_*.
Все DDL уже в schema.sql (v8.11 → v8.16, 4 commits 2ebe000/9b99d81/
b08e1ed/7f694f7/9cf380f).
- В schema.sql lead_charges FK на partitioned deals(id, received_at)
вынесен в самый конец файла (после section 5 с deals), DEFERRABLE
INITIALLY DEFERRED. Иначе DB::unprepared() выдаёт "deals не существует"
на load.
- Тесты в tests/Feature/Integration/ остаются — они проверяют
структурные свойства (column existence, constraint name, RLS via
pg_class) через information_schema, не зависят от того как именно
schema создалась.
Verification:
- migrate:fresh OK на обеих БД (liderra + liderra_testing)
- Pest: 445 tests / 443 passed / 2 skipped / 0 failed
(было 421 baseline + 24 новых для Tasks 1-5 = 443; +2 skipped browser tests)
- Larastan: 0 errors
- Pint: passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 13:50:39 +03:00
Дмитрий
9cf380f170
feat(db): create supplier_sync_log audit table (SaaS-level, append-only)
...
Append-only journal AJAX-синхронизаций с поставщиком crm.bp-gr.ru.
Используется для retry, отладки rt-project-* и алертов менеджеру.
- 9 columns: id, supplier_project_id (nullable FK SET NULL),
action, request_payload (jsonb), response_body (jsonb),
http_status, error_message, duration_ms, created_at
- 1 CHECK chk_supplier_sync_log_action
(create/update/delete/disable/session_refresh)
- 3 индекса: supplier_project_id, action, created_at
- REVOKE ALL FROM crm_app_user (DO $$ conditional)
- No RLS (SaaS-level)
Spec: §4.3
Plan: Task 5
Test: 4/4 passed (table, action enum, FK, no RLS).
Schema v8.15 → v8.16. Метрики: 60 таблиц (+1) / 108 индексов (+3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 13:42:46 +03:00
Дмитрий
7f694f78e0
feat(db): create lead_charges ledger (tenant-scoped RLS, FK to partitioned deals)
...
Append-only ledger списаний за каждый доставленный лид. Tenant-scoped
с RLS tenant_isolation (ENABLE + FORCE + USING/WITH CHECK).
- 8 columns: id, tenant_id, deal_id, deal_received_at, tier_no,
price_per_lead_kopecks, charged_at, created_at
- Composite FK lead_charges_deals_fk(deal_id, deal_received_at) →
deals(id, received_at) DEFERRABLE INITIALLY DEFERRED
(deals партиционирована — DEFERRABLE для атомарного deal+charge)
- 2 индекса: (tenant_id, charged_at), (deal_id, deal_received_at)
- RLS на (tenant_id = current_setting('app.current_tenant_id')::bigint)
- GRANT SELECT, INSERT для crm_app_user (без UPDATE/DELETE — append-only)
Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §7.4
Plan: docs/superpowers/plans/2026-05-10-supplier-foundation-plan.md Task 4
Test: 3/3 passed (table exists, composite FK to deals, RLS enforces
tenant isolation via testing_rls_user role).
Schema v8.14 → v8.15. Метрики: 59 таблиц (+1) / 105 индексов (+2) /
39 RLS (+1) / функции/триггеры без изменений.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 13:38:17 +03:00
Дмитрий
b08e1edb33
feat(db): create pricing_tiers table (7-step volume billing, kopecks integer)
2026-05-10 12:54:02 +03:00
Дмитрий
9b99d81deb
feat(db): create supplier_projects table (SaaS-level aggregate, B1/B2/B3 platforms)
...
Plan 1/5 Task 2 — SaaS-level агрегатная сущность для проектов у поставщиков.
Несколько Лидерра-tenant'ов могут шарить один supplier_project (sharing-model
spec §2.3): для site/call — по domain/phone; для sms — по (sender, keyword)
на B2 или (sender) на B3. RLS НЕ применяется (таблица не tenant-scoped),
defense-in-depth через REVOKE ALL FROM crm_app_user.
Колонки: platform, signal_type, unique_key (TEXT), supplier_external_id,
current_limit, current_workdays/regions (jsonb), sync_status (pending/ok/failed),
last_synced_at, inactive_since (TTL 180 дней), timestamps.
CHECK constraints (chk_supplier_projects_*):
- platform IN (B1, B2, B3)
- signal_type IN (site, call, sms)
- sync_status IN (pending, ok, failed)
- NOT (platform=B1 AND signal_type=sms) — B1 не поддерживает СМС
Indexes: UNIQUE(platform, unique_key); btree на sync_status, inactive_since.
Тесты: 6/6 (table+columns / unique / platform CHECK / sync_status CHECK / no RLS / no privileges).
Schema: v8.12 → v8.13. Метрики: 56→57 таблиц / 98→101 индексов; RLS/функции/триггеры без изменений.
2026-05-10 12:47:25 +03:00
Дмитрий
2ebe000271
feat(db): extend projects for supplier integration (signal_type, identifier, sms_senders, sms_keyword, delivered_in_month, b1/b2/b3 FK placeholders)
...
Plan 1/5 Task 1. Adds 8 columns + 3 CHECK constraints + 1 composite index
to projects table for supplier integration foundation. Schema bumped
v8.11 to v8.12. Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §2.1.
7/7 Pest tests pass; rollback verified.
2026-05-10 12:40:02 +03:00