Дмитрий
22ad20337a
feat(балансы): баланс поставщика = остаток номеров × 20 ₽
...
У кабинета crm.bp-gr нет денежного баланса — есть «Баланс ГЦК» (остаток номеров)
в выпадашке шапки (table.balancetbl). supplier-balance.js логинится, раскрывает
выпадашку, читает «Баланс ГЦК» -> {numbers}. Провайдер: деньги = numbers ×
number_price_rub (20 ₽/шт, подтверждено владельцем). Live: 3096 -> 61 920 ₽.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-28 07:54:10 +03:00
Дмитрий
88e816c576
feat(балансы): backend плитки балансов внешних сервисов
...
Ежедневный контроль баланса DaData/Поставщик/Yandex Cloud плиткой дашборда.
- Таблица external_service_balances (pgsql_supplier, BYPASSRLS, last-value upsert)
- BalanceHealth: чистая логика светофора (red <floor или <3д; amber <floor или <7д)
- BalanceProvider+DTO; провайдеры DaData(API)/YC(OAuth→IAM→billing)/Supplier(Playwright)
- RefreshExternalBalancesJob: изоляция провайдеров (try/catch), расписание 06:30 МСК
- AdminDashboardController::balances() + плитка в summary + topup_url (кнопка «Пополнить»)
- Тесты: BalanceHealth, 3 провайдера, джоба, endpoint (102 теста зелёные)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-28 07:12:14 +03:00
Дмитрий
d5c972c3f2
feat(админка): connection pgsql_admin под ролью crm_admin_user (Путь А)
...
AdminTenantsController/AdminBillingController ходят под default-подключением;
новое pgsql_admin (crm_admin_user, srv_bypass) даст им cross-tenant доступ
через middleware-переключатель (следующий коммит). На dev fallback на
DB_USERNAME. Test: pgsql_admin делит базовый pgsql-конфиг, роль из DB_ADMIN_*.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-27 06:37:42 +03:00
Дмитрий
662ebd6e8b
feat/db-path-a: прод переключён на Managed PG + verify-full SSL + хвосты закрыты
...
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
- config/database.php: добавлен sslrootcert (env DB_SSLROOTCERT) для sslmode=verify-full
- ПИЛОТ.md §3: боевая БД = Yandex Managed PG; старая локальная БД = откат >=7 дней
- etap3-prod-cutover-DONE: отчёт переезда (деньги ДО==ПОСЛЕ, HTTP200, изоляция, откат)
- cspell-words: +рус. жаргон из снимков
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-26 12:22:06 +03:00
Дмитрий
3b142f9375
fix(billing-security): хардненинг webhook ЮKassa + чистка admin-auth комментариев
...
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Webhook (PaymentWebhookController): строгий матч gatewayPaymentId===paymentId
(confused-deputy), проверка валюты RUB (WebhookVerifyResult.currency), IP-allowlist
services.yookassa.webhook_ip_allowlist (fail-open при пустом). web.php: убраны
устаревшие «MVP без auth» комментарии — saas-admin зона fail-closed (nginx-basic
+ M-1 REMOTE_USER allowlist, проверено на проде). +3 теста, 11/11 зелёные.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-23 04:15:48 +03:00
Дмитрий
1a0c9f5c8d
fix(ui): иконки-? → Lucide, почта поддержки .app→.ru, без префикса text:, карточка Канбана 0₽→—, crm.bp-gr.ru
...
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
UI-аудит раунд 2 (Playwright, протыкивание форм):
- vuetify.ts: +13 mdi→Lucide маппингов — bulk-бар проектов / импорт / экспорт отчётов и сделок / помощь / действия админки больше не падают в HelpCircle-fallback «?»
- config/services.php + ErrorMeta/ErrorView/HelpView: support@liderra.app → support@liderra.ru (домен продукта .ru); status.liderra.app → status.liderra.ru
- dealsApiMapper: ветка deal.commented — текст комментария в активности без служебного ключа «text:»
- KanbanCard: costKopecks null-aware — «—» вместо врущего «0 ₽» (как в drawer)
- DealsView: подзаголовок «crm.bp» → «crm.bp-gr.ru» (как в импорте/админке)
Верификация: type-check ✓, build ✓, переоткрыто в Playwright локально (иконки/почта/комментарий/карточка/подзаголовок).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-21 14:34:03 +03:00
Дмитрий
0c26afd03e
fix(admin): починка 500 на admin-сохранениях (audit-id из config, без saas_admin_users) + тариф текущей датой
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-06-21 11:32:53 +03:00
Дмитрий
7606e69dbc
fix(captcha): актуальный validate-URL Yandex SmartCaptcha smartcaptcha.cloud.yandex.ru (сверено по докам)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
2026-06-21 09:15:20 +03:00
Дмитрий
12eace3699
feat(security): реальный Yandex SmartCaptcha драйвер самозаписи по CAPTCHA_DRIVER (M-2)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-06-21 09:02:31 +03:00
Дмитрий
d784df50a8
fix(security): fail-closed app-гейт админ-зоны по REMOTE_USER и allowlist — M-1
2026-06-21 08:11:02 +03:00
Дмитрий
3e1eb7e835
feat(G7-B): guard impersonation + envelope машинного ключа на рабочих группах кабинета
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-06-19 16:44:42 +03:00
Дмитрий
15a66b52a9
feat(G7-A): конфиг support.email + jivosite.widget_id
...
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-19 14:50:39 +03:00
Дмитрий
08d51eb6c8
feat: G1/SP2 реквизиты клиента + ИНН по DaData + гейт первого проекта
...
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-18 22:25:23 +03:00
Дмитрий
53fb7b7760
feat: G1/SP1 самозапись клиента с подтверждением почты 6-значным кодом
...
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-18 19:33:33 +03:00
Дмитрий
8ad9e1d17f
fix(test): routing-snapshot today+tomorrow в CsvWebhookRaceTest + PII на slack/papertrail/stderr
...
C: LeadRouter.activeSnapshotDate после 21:00 МСК = завтра; снимок только на сегодня не активен -> снимки на обе даты. A: PII-процессор на остальные лог-каналы, 6/6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-17 21:37:41 +03:00
Дмитрий
7f5288726a
feat(security): PII-scrubbing процессор логов — Medium go-live
...
Monolog PiiScrubbingProcessor (телефоны/email -> [PHONE]/[EMAIL]) + ScrubPii tap на single/daily в config/logging.php. Pest 6/6 GREEN. Sentry-scrubbing (OPEN-И-16) не реализуем: sentry-laravel не установлен — open-item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com >
2026-06-17 20:43:28 +03:00
Дмитрий
c975e16a14
feat: merge lead-region cascade to main — deals-city + rossvyaz normalize, prod-parity verified
...
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-16 07:17:38 +03:00
Дмитрий
76ea9bbb04
feat(backend): Rector ( #64 ) + PHP Insights ( #65 ) install + configs
...
Rector: rector/rector ^2.4 + driftingly/rector-laravel ^2.3; app/rector.php
(deadCode+codeQuality, conservative). composer rector / rector:fix scripts.
dry-run baseline=16 files -> manual/CI posture, NOT blocking lefthook (ADR-013).
PHP Insights: nunomaduro/phpinsights; app/config/insights.php — SyntaxCheck removed
(Windows subprocess crash + redundant), style not gated (Pint owns, BT4),
security-check off. Baseline Code80/Complexity81/Arch75; floors set; composer insights -> 0.
allow-plugins += dealerdirect/phpcodesniffer-composer-installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-21 04:21:26 +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
Дмитрий
8fc9d3ec8a
feat(supplier): Plan 3 Task 4 — SupplierPortalClient HTTP-обёртка над rt-*
...
Компоненты:
- SupplierProjectDto (readonly DTO, fromModel + equals)
- SupplierException иерархия (Auth/Transient/Client + abstract base)
- SupplierAuthException: 401/403 sticky после refresh-retry
- SupplierTransientException: 5xx/network/timeout (retryable)
- SupplierClientException: 4xx 400/404/422 (наша ошибка payload)
- SupplierPortalClient: list/save/update/delete projects через Http facade
+ Redis cache cookie/CSRF (key 'supplier:session', TTL 6h)
+ auto-retry на 401/403 через dispatch_sync(RefreshSupplierSessionJob)
+ classification HTTP errors → 3 exception types
- RefreshSupplierSessionJob stub (handle() пустой; full impl в Task 5)
- config/services.supplier (login/password/portal_url/alert_email)
+9 тестов через Http::fake() в Unit/Supplier/SupplierPortalClientTest.php:
- cookie/CSRF attach
- 401 retry flow (single retry, sticky 401 → SupplierAuthException)
- 5xx → SupplierTransientException
- 4xx → SupplierClientException
- network error → SupplierTransientException
- save/update/delete payload shapes + responses
NOTE: toPayload() shape — placeholder; точные поля адаптируются
после Task 1 discovery + Task 2 spec §4.4 (отдельный fixup commit
перед Task 6 при расхождении наблюдаемого формата с предполагаемым).
2026-05-11 06:46:13 +03:00
Дмитрий
8c70255d2b
fix(supplier): Plan 3 Task 3 code-review fixes (4 Important + 3 Minor)
...
Закрывает 4 Important issues из code-review Task 3 (6d6181b ):
- config/database.php: inline 11-key duplication заменён на single-source
pattern через локальную переменную $pgsqlConnection (config() внутри
config-файла не работает — Repository ещё не bootstrap'нут); 'pgsql' и
'pgsql_supplier' теперь оба ссылаются на $pgsqlConnection; PDO options
block с string-key _role_purpose удалён (PDO ждёт integer ATTR_* keys)
- tests/Concerns/SharesSupplierPdo.php (новый): trait для cross-connection
PDO visibility в DatabaseTransactions; setUp override из TestCase.php
удалён (был global на 562 теста, forced eager PDO connect);
trait применён к 5 supplier-flow тестам: SupplierConnectionTest,
LeadRouterTest, RouteSupplierLeadJobTest, ResetDeliveredTodayCommandTest,
SupplierLeadFlowTest (все нуждаются в cross-connection видимости)
- phpstan-baseline.neon: entry для Pest TestCall->artisan() в
SupplierConnectionTest заменён на inline @phpstan-ignore-next-line
— local + self-documenting; добавлен baseline-entry для
SharesSupplierPdo trait.unused (PHPStan не видит Pest uses() как trait usage)
Plus 3 Minor:
- typos 'dafault'/'corretly' (удалились с setUp override из TestCase.php)
- RouteSupplierLeadJob.php PHPDoc: \$connection → DB_CONNECTION консистентность
Pest: 562 tests, 560 passed + 2 skipped (без regression). PHPStan: 0 errors. Pint: clean.
2026-05-11 01:26:24 +03:00
Дмитрий
6d6181b8cc
feat(supplier): Plan 3 Task 3 — switch supplier-flow на pgsql_supplier (BYPASSRLS)
...
Закрывает 3 backlog-айтема Plan 2.6 одной правкой:
- BLOCKER #6 : failed_webhook_jobs INSERT с tenant_id=NULL теперь проходит
(BYPASSRLS обходит RLS-политику отвергавшую NULL под обычной ролью)
- WARN #2 : LeadRouter::matchEligibleProjects видит projects всех tenant'ов
через Project::on('pgsql_supplier') без SET LOCAL app.current_tenant_id
- WARN #3 : ResetDeliveredTodayCommand обновляет projects всех tenant'ов
через DB::connection('pgsql_supplier')
Архитектура: crm_supplier_worker BYPASSRLS-роль (создана Plan 2.6 #iv 7899071 )
+ новый pgsql_supplier connection в config/database.php. WHERE(tenant_id=)
фильтры сохраняются как defense-in-depth.
Уточнение по Job's $connection: оригинальный план предполагал public $connection
= 'pgsql_supplier' на RouteSupplierLeadJob, но в Laravel Job's $connection
управляет очередью (sync/database/redis), не БД. Заменено на константу
RouteSupplierLeadJob::DB_CONNECTION + явный DB::connection(self::DB_CONNECTION)
в failed() callback'е. Это:
1) не ломает queue resolution (без этой правки тесты падают
'pgsql_supplier queue connection has not been configured')
2) явно документирует intent — failed_webhook_jobs INSERT идёт через BYPASSRLS
3) handle()'s tenant-scoped транзакции остаются на default pgsql + SET LOCAL,
где RLS нужна для defense-in-depth.
Также добавлено в tests/TestCase.php разделение PDO между pgsql и
pgsql_supplier connection'ами через setPdo/setReadPdo — иначе DatabaseTransactions
не откатывал бы supplier-side данные (две PDO-сессии = две независимые транзакции,
supplier не видит uncommitted INSERTs default-side).
Brainstorm decision: вариант C из 3 опций (A=schema bump, B=отдельная таблица,
C=BYPASSRLS-role). См. docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.
+4 теста в Feature/Supplier/SupplierConnectionTest.php (DB_CONNECTION constant +
BLOCKER#6 + WARN#2 + WARN#3). 0 schema changes.
Pest: 562/560 + 2 skipped (baseline 558/556 + 4 new = 562/560, ok). PHPStan: 0 errors
(добавлен 1 baseline entry для известного Pest+PHPStan limitation на artisan()).
Pint: clean.
2026-05-11 01:00:47 +03:00
Дмитрий
9c488122a1
phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
...
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 03:36:27 +03:00
Дмитрий
04b90afda4
phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
...
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
+ кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.
Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
validation + last_login_at update + register success/duplicate/без accept +
me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).
Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.
CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 19:41:35 +03:00
Дмитрий
4d38f75826
phase1(scaffold): Laravel 11 + predis + .env под PG 16 + Memurai
...
Триггер фазы 1 запущен 08.05.2026 (вечер):
composer create-project laravel/laravel app
Стек подтверждён native (без Docker/WSL2/Sail):
- PostgreSQL 16.13 (Chocolatey, Windows-сервис, port 5432)
- Memurai Developer 4.1.8 (Redis 7-совм., port 6379) — TCP +PONG OK
- PHP 8.3.31 + 11/11 Laravel-required ext (pdo_pgsql, mbstring,
openssl, tokenizer, xml, ctype, json, bcmath, fileinfo, curl, pgsql)
- Composer 2.9.7
Что в коммите (59 файлов, 11059 строк скаффолда Laravel 11 +
правки):
- composer require predis/predis (v3.4.2) — PHP-only Redis-клиент,
т.к. php_redis ext не установлен (см. project_phase1_strategy.md)
- app/.env (gitignored) — APP_NAME=Liderra, APP_LOCALE=ru,
APP_TIMEZONE=Europe/Moscow, DB_CONNECTION=pgsql → liderra@localhost,
REDIS_CLIENT=predis
- app/.env.example — те же правки без секретов (для команды)
Smoke-test PG ↔ Laravel ↔ pdo_pgsql прошёл:
3/3 default-миграций → 9 таблиц в liderra (cache, cache_locks,
failed_jobs, job_batches, jobs, migrations, password_reset_tokens,
sessions, users).
Артефакт стартера app/database/database.sqlite (0 B) удалён —
sqlite не используется.
Что НЕ в этом коммите (следующие шаги фазы 1):
- Pest 3 swap (CTO-12) — composer remove phpunit + require pest
- Laravel Boost MCP + 9 guidelines disable по CLAUDE.md §5/§7
- Pint, Larastan, Roave/SecurityAdvisories, IDE Helper, squawk,
pgFormatter (Прил. Н #11–18)
- resources/boost/guidelines/vuetify.blade.php
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 09:37:16 +03:00