Дмитрий
c025ec4b69
feat(projects-bulk): update_days handler with bitmask OR/AND-NOT
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 14:52:23 +03:00
Дмитрий
8220a85a5d
feat(projects-bulk): update_regions handler with bitmask OR/AND-NOT
...
Refactor ProjectService::bulkAction to accept full payload array and
return structured {updated, skipped, warnings}. Add bulkUpdateRegions
using PG raw bitmask expr (region_mask | add) & ~remove & 255.
Add stubs for bulkUpdateDays/bulkUpdateLimit (Tasks 3-4). Update
controller to pass merged payload and return service result directly.
Un-todo Task-1 region validation test; add regions bitmask test (18/20).
Update phpstan-baseline: actingAs count 5->6, restore match.unhandled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 14:48:10 +03:00
Дмитрий
08f02100fe
fix(projects-bulk): treat empty scope.filter as valid scope
...
Replace !empty() check with has()+is_array() so scope:{filter:{}} is
accepted as "all projects" rather than rejected as missing selection.
Expand scope.filter to IDs in the controller (500-row limit guard) so
the service receives a typed array[]; add Pest coverage for this case.
Update phpstan baseline count for new actingAs() call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 14:43:45 +03:00
Дмитрий
40202caf34
feat(projects-bulk): extend validation for 6 actions + scope
...
- BulkProjectActionRequest: add update_regions/update_days/update_limit actions, scope.filter, withValidator for ids-or-scope + delta/replace mutual exclusion
- ProjectBulkActionsTest: 4 new tests (3 pass, 1 todo pending Task 2 service handler)
- ProjectsActionsTest: update > 100 ids limit test to match new max:500
- phpstan-baseline: add 4 actingAs false-positive entries for new test file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 14:38:59 +03:00
Дмитрий
5c8ad2738a
feat(layout): dark topbar + sidebar cleanup + DevIndexBadge moved below
...
Sidebar: убраны Менеджеры/Напоминания; Работа в порядке
Проекты/Сделки/Канбан/Дашборд; Команда — только Настройки;
снят useRemindersStore (был только под reminders badge).
Topbar: тёмный фон linear-gradient(noir → #04261E) совпадающий
с sidebar #1271 ; убран breadcrumb «Рабочая область»;
v-toolbar__content padding-left:240 (не уходит под sidebar).
DevIndexBadge: top:64 (ниже топбара, не перекрывает user-chip).
Vitest AppLayout 15/15 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 14:32:03 +03:00
Дмитрий
d238ca5f4a
feat(dev-indices): overlay Alt-keys (up/down) + Alt+Shift+I toggle + mini-badges
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 12:05:53 +03:00
Дмитрий
d8c33b4cd6
feat(dev-indices): DevIndexOverlay (hover badge + click-copy + Esc + AppShell mount)
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 12:02:38 +03:00
Дмитрий
901530ae41
feat(dev-indices): useDevIndices composable (state + DOM walk)
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-12 11:57:35 +03:00
Дмитрий
e2669270f3
feat(redesign): Task 17 — ProjectCard tokens (hover lift + JetBrains Mono numerics)
2026-05-12 10:22:36 +03:00
Дмитрий
22e6bdf8b8
feat(redesign): Task 16 — KanbanView StatusPill + hover lift (motion #4 )
...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 10:19: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
Дмитрий
a09434eca0
feat(redesign): Task 13 — page transition wiring (Vue Transition + CSS fadeup, motion #6 )
2026-05-12 09:59:22 +03:00
Дмитрий
3f956224bd
feat(redesign): Task 12 — AppSidebar двухтоновый shell + ⌘K stub + active marker (motion #7 )
2026-05-12 09:54:54 +03:00
Дмитрий
2707ff64ab
feat(redesign): Task 11 — DensityToggle component (compact/comfortable + persist)
2026-05-12 09:49:37 +03:00
Дмитрий
0b2ec5b802
feat(redesign): Task 10 — FilterChip component (label + count + active states)
2026-05-12 09:47:02 +03:00
Дмитрий
52cc64c9e6
feat(redesign): Task 9 — Kbd component (⌘K, Esc badges; light+dark variants)
2026-05-12 09:45:22 +03:00
Дмитрий
ff3bc8bcc1
feat(redesign): Task 8 — StatusPill component + 14-variant Histoire story
2026-05-12 09:43:02 +03:00
Дмитрий
7322c7f33a
feat(redesign): Task 7 — useDensity composable (localStorage + rowHeight)
2026-05-12 09:37:43 +03:00
Дмитрий
eda13679b4
feat(redesign): Task 6 — useCountUp composable (RAF tween + prefers-reduced-motion)
2026-05-12 09:34:55 +03:00
Дмитрий
cdd1b5efdb
feat(redesign): Task 5 — useStatusPill composable (14 slugs из db/schema.sql)
2026-05-12 09:29:53 +03:00
Дмитрий
ea4570dafe
feat(redesign): Task 4 — extend Vuetify theme (12 colors) + global component defaults
2026-05-12 09:27:22 +03:00
Дмитрий
b858df569e
feat(redesign): Task 3 — motion.css (5 keyframes + reduced-motion wrapper + utilities)
2026-05-12 09:23:50 +03:00
Дмитрий
baf27bd02d
feat(redesign): Task 2 — typography.css (Inter variable + JetBrains Mono + tnum)
2026-05-12 09:20:13 +03:00
Дмитрий
688d9cfb24
feat(redesign): Task 1 — tokens.css (12 colors + spacing + radii + shadows)
2026-05-12 09:15:29 +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
Дмитрий
458fa0b84d
feat(projects): Plan 5 Task 6 — destroy + sync + toggle-active + bulk endpoints
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 19:06:07 +03:00
Дмитрий
6238b8b580
feat(projects): Plan 5 Task 5 — update + UpdateProjectRequest + resync trigger
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 19:00:39 +03:00
Дмитрий
85f8e9e7a0
feat(jobs): Plan 5 Task 4 — SyncSupplierProjectJob full impl + ensureSupplierProject
...
- SyncSupplierProjectJob: replace stub with full implementation
(tries=3, backoff=[15,60,300]s; resolvePlatforms uppercase B1/B2/B3;
buildUniqueKey site/call→signal_identifier, sms B2→sender+keyword, B3→sender;
column name via strtolower($platform) to match schema snake_case)
- SupplierPortalClient: drop final modifier (Mockery testability);
add ensureSupplierProject() idempotent lookup-or-create wrapper
- Tests: 6 passing (site/call/sms-with-kw/sms-no-kw/exception/partial-failure);
DI fix via dispatchJobSync() helper resolving mock from container;
uppercase platform fixtures matching CHECK constraint B1/B2/B3;
last_error column absent from schema — partial-failure test uses sync_status only
- phpstan-baseline.neon: add $this->mock() Pest TestCase inference gaps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 18:52: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
Дмитрий
e242e7d7fc
fix(projects): Plan 5 Task 2 code-review fixes (2 Important + 2 Minor)
...
I-1/M-1: introduce resolvedSupplierProjects() private helper on Project
model; rewrite aggregateSyncStatus(), aggregateLastSyncedAt(),
getSupplierLinks() to read from eager-loaded supplierB1/B2/B3 relations
instead of SupplierProject::find() — eliminates up to 120 SELECTs/page.
I-2: aggregateLastSyncedAt() now uses sortBy(timestamp) instead of
Collection::min() on Carbon objects (string-comparison was unreliable).
M-2: add explanatory comment on intval+array_filter silent-drop behaviour
in the ?ids batch-fetch path.
M-3: new test — ?ids batch silently excludes foreign-tenant project IDs.
M-4: new test — show returns 200 for archived project (read preserved).
PHPStan baseline updated: 2 new test functions raise actingAs() count 7→9.
Tests: 9/9 passed (33 assertions). Larastan: 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 18:15:36 +03:00
Дмитрий
35310b5517
feat(projects): Plan 5 Task 2 — index expanded (filters/search/pagination/ids) + show
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 18:08:01 +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
Дмитрий
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
Дмитрий
deca81c2d7
feat(supplier): Plan 4 Task 8 — CsvReconcileJob hourly + drift>5% email + supplier_csv_reconcile_log
...
CsvReconcileJob — hourly резерв-канал приёма лидов через CSV-экспорт поставщика:
- Cache::lock 600s (overlap protection).
- Окно [now-25h, now] (запас 1ч над hourly cron).
- INSERT supplier_csv_reconcile_log status='running' → 'ok' | 'drift_alert' | 'failed'.
- Missing vids → INSERT supplier_leads (platform extracted из project, source='csv_recovery',
recovered_from_csv_at=now) + dispatch RouteSupplierLeadJob.
- Drift > 5% → CsvDriftAlertMail на services.supplier.alert_email.
- UNIQUE-vid conflict → log + skip (idempotency).
- На SupplierTransientException/любой Throwable → status='failed', error_message, rethrow.
CsvDriftAlertMail + blade-template emails/csv_drift_alert.
routes/console.php — Schedule::job(new CsvReconcileJob)->hourly().
config/services.php — supplier.alert_email default 'ops@liderra.ru '.
6 integration tests (CsvReconcileJobTest) + Schedule registration test (через
Http::fake + Bus::fake + Mail::fake + SharesSupplierPdo trait для cross-connection).
Parallel-test race fix: putSupplierSession() вызывается прямо перед SUT, потому
что Sync/Cleanup tests'ы в afterEach делают forget('supplier:session'), а в
--parallel режиме воркеры делят Redis DB+prefix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 11:04:49 +03:00
Дмитрий
cb86065588
feat(supplier): Plan 4 Task 7 — SupplierCsvParser (streaming) + SupplierPortalClient::downloadLeadsCsv
...
SupplierCsvParser — pure streaming-generator CSV parser:
- BOM (UTF-8 EF BB BF) + CRLF normalization
- Malformed rows (< 6 columns) skipped + Log::warning
- 5 unit tests: empty / 1 row / 1000 rows / malformed / BOM+CRLF
SupplierPortalClient::downloadLeadsCsv(CarbonInterface, CarbonInterface):
- GET /admin/report/index?type=49 через существующий request() helper
- Наследует auth/retry семантику (401 → RefreshSession, 5xx → Transient, 4xx → Client)
- 3 unit tests через Http::fake: 200 / 401 retry / 500
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 10:50:11 +03:00
Дмитрий
ce87936f44
feat(billing): Plan 4 Task 6 — auto-pause flow + ZeroBalancePausedMail + 1/hour rate-limit
...
При InsufficientBalanceException в LedgerService::chargeForDelivery:
- DB::transaction откатывается (Deal/charge/balance не тронуты).
- Outer catch в createDealCopyForProject вызывает handleInsufficientBalance:
* UPDATE projects.is_active=false через pgsql_supplier (BYPASSRLS).
* Email ZeroBalancePausedMail через NotificationService::notifyZeroBalancePaused.
* Rate-limit 1/час/tenant через Redis SETNX (Cache::add).
* Log::warning с tenant_id/project_id/balance details.
- Возвращаем false (не rethrow), чтобы handle()-loop продолжал routing остальным tenant'ам.
5 тестов: project paused / email sent / rate-limit 1/h / 2nd email after 65min /
sharing-flow isolation (A paused, B receives).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 10:43:51 +03:00
Дмитрий
dadfdcaa7e
feat(commands): Plan 4 Task 5 — ResetMonthlyCountersCommand + Schedule monthlyOn(1, 00:00) МСК
...
Месячный cron-сброс tenants.delivered_in_month + projects.delivered_in_month
1-го числа каждого месяца в 00:00 МСК. Идёт через pgsql_supplier BYPASSRLS
connection (паттерн ResetDeliveredTodayCommand). Идемпотентный
(WHERE delivered_in_month <> 0 → повторный запуск 0 affected rows).
4 теста: reset multi-tenant + idempotency + Schedule registration +
BYPASSRLS without SET LOCAL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 10:28:13 +03:00
Дмитрий
e401491947
feat(supplier): Plan 4 Task 4 — integrate LedgerService в RouteSupplierLeadJob + Task 3 carry-overs
...
Task 4 — integration:
- handle() / createDealCopyForProject() — +5-й параметр LedgerService.
- Заменён старый balance_leads-- + BalanceTransaction блок на
\$ledger->chargeForDelivery(\$tenant, \$deal, \$lead) с try/catch для
InsufficientBalanceException (Log::warning + rethrow; auto-pause flow
в Task 6).
- LeadRouter::matchEligibleProjects — расширен фильтр tenant balance с
(balance_leads > 0) на (balance_leads > 0 OR balance_rub > 0), чтобы
rub-only tenant дошёл до LedgerService (single arbiter for dual-balance).
- 4 E2E теста в tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php:
prepaid charge + BalanceTransaction (carry-over M-2), rub charge + BT,
supplier_lead_costs gap-fix (2 deal-копии), retry idempotency.
Plan 4 Task 3 carry-overs (минорные правки по code-review d2030f9 ):
- I-2: PHPDoc на LedgerService::chargeForDelivery — @throws + @precondition
(caller wraps в DB::transaction с lockForUpdate Tenant).
- I-4: trim() на raw_payload['project'] в resolveSupplierId (defense
against whitespace).
Прочие правки:
- tests/Feature/Jobs/RouteSupplierLeadJobTest.php — +PricingTierSeeder
в beforeEach + +5-й LedgerService параметр в runRouteJob().
- tests/Feature/Integration/SupplierLeadFlowTest.php — +PricingTierSeeder
в beforeEach (test использует full webhook→job pipeline).
- tests/Feature/Services/LeadRouterTest.php — rename теста про balance_leads
→ \"zero in BOTH balance_leads AND balance_rub\" + новый тест
\"rub-only tenant ДОЛЖЕН пройти\".
- phpstan-baseline.neon — +5 entries для TestCall::seed() + Tenant/LeadCharge
property.notFound в новых файлах (IDE helper @mixin re-generation —
отдельная задача).
Метрики: Pint clean, PHPStan 0 errors, Pest 646/643+3 skipped/0 failed
(21.1s parallel). Plan 4 Task 4 закрыт.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 10:06:38 +03:00
Дмитрий
d2030f9121
feat(billing): Plan 4 Task 3 — LedgerService::chargeForDelivery (dual-balance + lead_charges/supplier_lead_costs INSERT)
2026-05-11 09:42:29 +03:00
Дмитрий
1e0c0ab90a
fix(billing): Plan 4 Task 2 code-review fixes (2 Important + 1 Minor)
...
- PricingTierResolver::resolveForCount — InvalidArgumentException на
$leadOrdinal < 1 (closes I-1: defensive contract validation).
- PricingTierRepository::activeAt — explicit @var Collection<int,
PricingTier> annotation для type narrowing (closes I-2; firstOrFail
отвергнут — Stan ругается на Eloquent\Model return-type).
- PricingTierResolverTest — +1 unit test (8/8 PASS): throws на 0/-1.
- PricingTierRepositoryTest — +1 integration test (5/5 PASS): excludes
inactive tiers (closes M-2 coverage gap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:17:13 +03:00
Дмитрий
e07d025efd
feat(billing): Plan 4 Task 2 — PricingTierResolver + Repository (pure resolver + DB-обёртка)
...
- PricingTierResolver: pure-function, ищет tier для N-го лида (1-based).
100→tier1, 101→tier2, 6000→tier6 (cumul.sum 1-6), 6001+→tier7 (NULL=unlimited).
RuntimeException на пустой коллекции.
- PricingTierRepository::activeAt(Carbon): DB-обёртка, MAX(effective_from) <= $at
per tier_no (учёт «новая сетка перекрывает старую»), is_active=true.
- 7 unit-тестов (in-memory, без БД) + 4 integration-теста (DatabaseTransactions
с baseline-cleanup для seed-7-tiers из PricingTierSeeder Task 1).
- phpstan-baseline.neon: +3 entry (Pest TestCall::\$resolver/\$tiers/\$repo) —
следуем project-convention (см. SupplierResolverTest идентичные baseline-entries).
Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §3.1
Plan: docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin.md Task 2
Tests: 633 passed / 3 skipped / 0 failed (+11 new); pint clean; stan 0 errors above baseline.
2026-05-11 09:05:22 +03:00
Дмитрий
1e3c157603
fix(test): Plan 4 Task 1 regression — PricingTierTest::scopeActive baseline-aware
...
Task 1 (a907fea ) добавил $this->call(PricingTierSeeder::class) в DatabaseSeeder,
который автоматически seed'ит 7 ступеней при migrate:fresh --seed на dev/testing.
Существующий PricingTierTest::scopeActive ожидал empty pricing_tiers до factory,
поэтому ->count() == 1 → 8 fail в isolation (pest tests/Feature/Models/PricingTierTest.php).
Fix: scopeActive теперь считает baseline = PricingTier::active()->count() ДО factory create,
ожидает baseline + 1 после factory (1 активный + past, 1 inactive, 1 active + future).
PricingTierTest::current() — не затронут: keyBy('tier_no') корректно перезаписывает
seed-rows (effective_from='1970-01-01') свежими factory-rows (effective_from=now()->subDay()).
Verified:
- pest tests/Feature/Models/PricingTierTest.php — 4/4 PASS, 12 assertions
- pest --parallel — 619 passed / 3 skipped / 0 failed / 1923 assertions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 08:59:06 +03:00