Commit Graph

224 Commits

Author SHA1 Message Date
Дмитрий 564d984f2a fix(auth): registerResend — общий часовой лимит отправок (review)
Backend-ревью: register/resend применял только cooldown 60с, но не часовой
лимит 5/час (spec §7.6) — можно было слать код 1/мин бесконечно. Добавлен
RateLimiter по ключу email|ip (как в registerStart). +тест throttle для start.
Larastan baseline перегенерирован (новый тест добавил postJson-вызовы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:21:14 +03:00
Дмитрий fdff36c553 refactor(auth): убрать одношаговый register (заменён start/verify)
Удалён старый POST /api/auth/register (метод + роут + RegisterRequest + 3 теста);
регистрация теперь только через register/start → register/verify. SPA-роут
страницы /register сохранён. Larastan baseline перегенерирован (counts
AuthControllerTest 9→8 / 14→10 после удаления тестов).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:13:11 +03:00
Дмитрий 3711a92958 feat(auth): регистрация — подтверждение email кодом + обязательный телефон (backend)
PhoneNormalizer (RU-телефон → 7XXXXXXXXXX) + Mailable RegisterEmailVerificationCode
с 6-значным кодом + эндпоинты register/start|verify|resend: pending-регистрация
в сессии (паттерн 2FA), email_verified_at=now() при verify, rate-limit на start +
cooldown 60с на resend, лимит 5 попыток ввода кода. Телефон обязателен, нормализуется
в 7XXXXXXXXXX. deptrac: разрешён Request→Service. Старый одношаговый register пока
сохранён (удаляется отдельной задачей Task 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:54:23 +03:00
Дмитрий 1df353ae51 fix(supplier): SyncSupplierProjectJob → pgsql_supplier (BYPASSRLS) — иначе queue-воркер падает 42704
Джоб создания/правки проекта запускается из очереди, где SetTenantContext не
отрабатывает (нет app.current_tenant_id GUC). Под боевой ролью crm_app_user первый
же Project::find() падал SQLSTATE 42704 (unrecognized configuration parameter
app.current_tenant_id) за ~2мс — до контакта с поставщиком: проект у поставщика не
создавался, в UI вечный «Sync pending». На dev не всплывало (postgres superuser
обходит RLS). Единственный supplier-flow джоб, который был на дефолтном подключении.

Фикс: const DB_CONNECTION = 'pgsql_supplier' + все DB-операции через ::on()/
DB::connection() — как у SyncSupplierProjectsJob/DeleteSupplierProjectJob/CsvReconcileJob.

Тесты: SupplierConnectionTest +constant-assert; SyncSupplierProjectJobTest
+поведенческий connection-assert (DB::listen → projects-запросы на pgsql_supplier);
Plan5/SyncSupplierProjectJobTest +SharesSupplierPdo (джоб теперь пишет через
pgsql_supplier → нужен shared PDO под DatabaseTransactions).

Проверено вживую на тест-сервере: проекты 14/15 синхронизированы, 6 доноров у
crm.bp-gr.ru (12742042-44 / 12766120-22), aggregateSyncStatus=ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:02:40 +03:00
Дмитрий e6752b5e4c feat(deploy): temporary SAAS_ADMIN_TEST_BYPASS flag for test server (off by default)
Allows SaaS-admin area in non-local/testing envs only when SAAS_ADMIN_TEST_BYPASS=true.
Default false -> production unaffected. Remove after Yandex 360 SSO (Б-1 + DO-4).
TDD: tests/Feature/Middleware/EnsureSaasAdminTest.php (2 passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:00:16 +03:00
Дмитрий 1220bddf3e fix(supplier): recreate deleted donor + fill legacy FK in online sync
handleOnline/syncGroup: сверка external_id со списком живых проектов портала (listProjects); пересоздание удалённых на портале доноров in-place без удаления записей (на supplier_projects могут висеть лиды/списания). online-режим заполняет supplier_b1/b2/b3_project_id, чтобы UI sync-бейдж не залипал в pending. +3 Pest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:59:37 +03:00
Дмитрий 07d73870ba refactor(projects): remove archive feature, drop archived_at column (schema v8.27) 2026-05-21 08:24:25 +03:00
Дмитрий 7408bc4232 feat(projects): hard delete with deals-guard, replace archive
- ProjectService: add delete() with DB-level deals check (bypasses SoftDeletes
  scope via DB::table), captures supplier pivot IDs before cascade, dispatches
  DeleteSupplierProjectJob; add bulkDelete() private method; replace archive
  match arm with delete; remove archive() method
- ProjectController: destroy() calls delete() not archive(); update docblocks
- BulkProjectActionRequest: replace 'archive' with 'delete' in Rule::in for action
- Tests: ProjectDeleteTest (2 new TDD tests), ProjectsActionsTest updated
  (destroy → hard delete, 409-already-archived → 422-has-deals, bulk archive → bulk delete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 08:08:33 +03:00
Дмитрий 9d68fc0ad6 feat(supplier): delete/re-sync donor on project delete respecting sharing
DeleteSupplierProjectJob: если после удаления Лидерра-проекта у донора
(supplier_project) не осталось других потребителей (pivot
project_supplier_links) — удаляет его у поставщика и локально;
если потребители есть — НЕ удаляет, диспатчит SyncSupplierProjectsJob.
2 Pest-теста (no-consumers / remaining-consumers) GREEN.
phpstan-baseline: +once() Mockery chain (аналог andThrow baseline).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 07:50:11 +03:00
Дмитрий e2fb20ef05 feat(projects): source+name dedup on update 2026-05-21 07:35:11 +03:00
Дмитрий 5427cdc740 feat(projects): source+name dedup with human messages on create 2026-05-21 07:01:46 +03:00
Дмитрий f3250ce178 feat(errors): global QueryException handler returns human message 2026-05-21 06:42:38 +03:00
Дмитрий 95ee6644f7 fix(tests): sync 3 stale эпик-тестов + schema.sql header под Plans 1-3 (v8.26)
Три pre-existing красных теста (ЭТАЛОН §6 «deferred») приведены к реальной
схеме v8.26 после project-migration-redesign Plans 1-3:
- SchemaDeltaTest: 64→65 base tables, 121→123 indexes (project_supplier_links
  pivot + supplier_projects_platform_key_subject_unique).
- SupplierProjectsAccessTest: unique-constraint (platform, unique_key) →
  (platform, unique_key, subject_code) — per-субъект экспорт (Plan 1).
- SupplierLeadFlowTest: routing eligibility теперь через pivot
  project_supplier_links (LeadRouter), не legacy supplier_b1_project_id —
  добавлены linkProjectToSupplier() связи.
- schema.sql header: v8.25→v8.26 + метрики (CHANGELOG уже содержал v8.26).

Production-код не менялся — тесты отставали от уже-смердженных Plans 1-3.
Pest full 1013/1010 passed/3 skipped/0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:23:13 +03:00
Дмитрий a0e18a1dd8 fix(supplier): matching по content в saveProjectMultiFlag — реальный портал возвращает name=B1_X
Реальный портал отдаёт rt-projects-load с name='B1_<id>' / 'B2_<id>' / 'B3_<id>'
и чистым идентификатором в поле 'content'. Старое matching по name === uniqueKey
никогда не совпадало с реальным ответом → idMap пустой → SyncSupplierProjectJob
молча выходил, ничего не записав в БД, а на портале оставались orphan-группы.

Объясняет ранее задокументированное в ЭТАЛОН «проект 5 вылечен вручную —
усыновлены 3 портальные записи». Заказчик обходил тот же баг руками.

Фикс — matching по content с fallback на name, чтобы мок-тесты с упрощённым
форматом (без content) продолжали работать; реалистичная фикстура добавлена
в SupplierPortalClientMultiFlagTest.

Verified:
- Pest supplier suite (SyncSupplierProjectJob/SyncSupplierProjectsJob/multi-flag): 16/16 passed
- E2E live на crm.bp-gr.ru: ProjectService::create + sync → supplier_projects записаны
  с ext_id, pivot заполнен, портал имеет 3 группы B1/B2/B3
- Multi-tenant ночной батч с computeOrder проверен на 79991177889 (T1+T2+T3+T4
  на одном identifier — формула max(max, ceil(Σ/3)) сходится с фактом)
2026-05-20 18:42:20 +03:00
Дмитрий 80275c6417 fix(supplier): real workdays from delivery_days_mask + resync on limit/days change
Закрывает два бага sync поставщика, обнаруженные при live-проверке создания
проекта «мой номер» (call, 79135191264, лимит 15, дни Пн-Пт):

1. SyncSupplierProjectJob хардкодил workdays=[1..7] в 7 местах и в DTO для
   portal, и в supplier_projects.current_workdays. Заменено на реальную маску
   через приватный workdaysFromMask() (зеркало bitmaskToList ночного батча).

2. forceFill в update-path online mode не включал current_workdays — после
   первого create со старыми [1..7] последующий ресинк не подтягивал
   реальные дни в локальную БД (на portal летели корректные, в нашей таблице
   оставались stale).

3. ProjectService::update() ресинкал только при смене sms_*/signal_identifier/
   regions. Добавлены daily_limit_target и delivery_days_mask — поставщик
   видит новый лимит и дни сразу, не дожидаясь ночного батча 18:00 МСК.

Тесты:
- SyncSupplierProjectJobTest: +2 specs (real-workdays create-path, update-path
  current_workdays refresh).
- ProjectsUpdateTest: «without resync» переписан в name-only, +2 specs
  (daily_limit_target и delivery_days_mask change → resync).
- Pest 146/146 (Supplier + Plan5/Projects scope), Pint passed, Larastan 0.

Live-ресинк проекта id=5 «мой номер» в dev DB выполнен — current_workdays
теперь [1,2,3,4,5], HTTP ушёл к crm.bp-gr.ru с теми же днями.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:33:46 +03:00
Дмитрий 36c71ecb1e fix(supplier): одна группа на идентификатор — сливаем все регионы проекта
Портал crm.bp-gr.ru возвращает status=Doubles при попытке создать
вторую группу с тем же unique_key. Старый код делал одну B1/B2/B3-группу
на каждый регион проекта — вторая группа молча пропадала.

Теперь оба джоба (SyncSupplierProjectJob + SyncSupplierProjectsJob)
формируют ровно одну группу на идентификатор со всеми регионами:
- regions=[82,83] → tag='РФ', regions=[82,83] в одной группе
- regions=[] → tag='РФ', regions=[] (вся РФ)
- regions=[82] → tag='Москва', regions=[82]
subject_code=null во всех supplier_projects и project_supplier_links.

ProjectService::update() теперь триггерит SyncSupplierProjectJob
при изменении поля regions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:46:27 +03:00
Дмитрий d0eecbbf79 feat(admin): supplier projects list (orderers, last delivery) + bulk delete
План 4 Task 2 эпика project-migration-redesign.

- AdminSupplierIntegrationController +projectsIndex (список supplier_projects
  + кто заказывал через pivot project_supplier_links -> projects -> tenants
  organization_name + дата последней поставки = max supplier_leads.received_at
  + subject_name из RussianRegions::CODE_TO_NAME, «РФ» при NULL subject_code).
- +projectsDestroy (bulk-delete: deleteProject на портале, затем локально;
  pivot снимается CASCADE; сбой строки не прерывает batch -> failures[]).
- Routes: GET /projects, POST /projects/delete в admin-группе.
- Pest 5/5 (26 assertions). phpstan-baseline +9 ignore (Pest TestCall).
2026-05-20 14:34:23 +03:00
Дмитрий 01d292f5a9 feat(admin): supplier export-mode toggle (online|batch) endpoint + UI
План 4 Task 1 эпика project-migration-redesign.

- AdminSupplierIntegrationController +getExportMode/setExportMode
  (validation in:online,batch; system_settings upsert).
- Routes: GET/POST /api/admin/supplier-integration/export-mode
  в admin-группе рядом с manual-queue.
- AdminSupplierIntegrationView.vue +секция «Режим экспорта проектов»
  с v-btn-toggle (online|batch), подпись о ночном синке 18:00.
- Pest 3/3 + Vitest 2/2 (+ соседние 5 не сломаны).
- phpstan-baseline.neon +6 ignore (Pest TestCall::actingAs/getJson/postJson
  — типовой паттерн, как в SupplierManualQueueTest).
2026-05-20 14:34:22 +03:00
Дмитрий 082968ea1c feat(supplier): online-mode full-param per-subject sync + grouping helpers 2026-05-20 12:34:27 +03:00
Дмитрий 2d7201f063 feat(supplier): SyncSupplierProjectsJob per-subject grouping + pivot + order 2026-05-20 12:24:35 +03:00
Дмитрий 96f4a6601d feat(supplier): saveProjectMultiFlag R5 + tag/platforms DTO (R6/R7) 2026-05-20 12:16:14 +03:00
Дмитрий 3cf8fbdfb9 feat(supplier): SupplierExportMode toggle resolver (online|batch) 2026-05-20 11:57:59 +03:00
Дмитрий d6364dcde1 refactor(tests): consolidate linkProjectToSupplier helper to tests/Pest.php (Plan 2 review I-1/I-2) 2026-05-20 11:52:47 +03:00
Дмитрий d631646167 feat(supplier): RouteSupplierLeadJob cap=3 distribution + deal.subject_code from tag 2026-05-20 11:46:24 +03:00
Дмитрий 2706166f55 test(supplier): pivot-link AutoPause+Billing tests + redeclare guard (Plan 2 cascade) 2026-05-20 11:46:13 +03:00
Дмитрий 6b7f0035ef feat(supplier): LeadRouter eligibility via pivot, drop phone region filter 2026-05-20 11:26:40 +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
Дмитрий c5def50e31 feat(supplier): Project<->SupplierProject belongsToMany via pivot 2026-05-20 11:04:24 +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
Дмитрий 79d3f2ef3d test(supplier): isolate subject_code test (DatabaseTransactions+SharesSupplierPdo) 2026-05-20 10:48:32 +03:00
Дмитрий 82c0aeef41 feat(supplier): supplier_projects.subject_code + per-subject unique index (v8.26) 2026-05-20 10:45:02 +03:00
Дмитрий 895975482d test(supplier): cover FailoverProjectChannel tier-3 escalation + transient bypass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 17:31:11 +03:00
Дмитрий 9b4bff48f0 fix(supplier): normalize rt-projects-load — dedup contract (code-review C1)
Финальное code-review вскрыло CRITICAL: dedup-сверка findOnPortal и
manualQueueResolve матчат строки портала по {platform, signal_type,
unique_key}, но listProjects отдавал сырое тело rt-projects-load.

Сырая форма (verified из recon-снапшота 2026-05-19):
- ответ — конверт {projects:[443 строки], tags, users, ...}, НЕ голый массив
  → listProjects возвращал весь dict, findOnPortal итерировал по ключам
  конверта (projects/tags/...) вместо строк проектов;
- строка проекта: {id, name:"B<n>_<key>", type:"hosts|calls|sms", content}
  — без platform/signal_type/unique_key.

Фикс:
- SupplierPortalClient::listProjects — извлекает body['projects'].
- AjaxProjectChannel::listProjects — нормализует сырые строки в контракт
  SupplierProjectChannel: platform <- префикс name "B<n>_", signal_type <-
  type (hosts->site/calls->call/sms->sms), unique_key <- content. Сырые
  поля сохранены. findOnPortal + manualQueueResolve матчат корректно.
- AjaxProjectChannelTest — тест нормализации против фактической формы
  портала (не идеального мока); SupplierPortalClientRtProjectTest —
  listProjects против конверта {projects}.

Также (code-review I1): findOnPortal catch — Log::warning проглоченного
исключения, иначе провал дедупа невидим (молчаливый дубль rt-проекта).

Code-review I2 (partial-unique индекс supplier_manual_sync_queue от
дубль-эскалаций при job-retry) и I3 (lockForUpdate в manualQueueResolve) —
follow-up до прод-релиза (эпик гейтится Б-1, не в проде).

Регрессия Pest 973/970/0 / 3 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:09:48 +03:00
Дмитрий 6c30c248bc fix(supplier): larastan deadCode + Pest higher-order cleanup (T12)
FailoverProjectChannel: убран unreachable throw new LogicException после
try-catch в createProjectForLiderra — все ветки уже терминируют (return /
throw WindowDeferred / escalateToTier3(): never). phpstan deadCode.unreachable.

SupplierManualQueueTest: test()-> заменён на $this-> (идиома проекта,
как AdminPricingTiersControllerTest) — phpstan не типизирует Pest
higher-order test(); authAdmin() helper убран, actingAs inline в it().

Изолированный phpstan по supplier-failover файлам — 0 реальных ошибок.
Task 12 cleanup. Channel-тесты 7/7, admin-тесты 4/4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:11 +03:00
Дмитрий 25088e4a33 feat(supplier): admin endpoints for Tier 3 manual queue
GET /api/admin/supplier-integration/manual-queue — pending список (limit 100).
POST /manual-queue/{id}/resolve — оператор пометил, что вручную создал проект
на портале; reconcile через channel->listProjects() по (platform, signal_type,
unique_key), 409 если не найден.

ОТКЛОНЕНИЕ ОТ plan Step 10.3: план писал portal external_id прямо в
projects.supplier_b*_project_id (FK на local supplier_projects.id) — FK
violation. Resolve делает firstOrCreate local supplier_projects row с
verified external_id, в FK пишет local id.

Routes — в группе saas-admin (web.php, EnsureSaasAdmin стаб). Task 10 of 12.
Tests 4/4 (index pending / exclude resolved / resolve match / resolve 409).

Spec §4.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:09 +03:00
Дмитрий fcd06afcb2 feat(supplier): retime supplier sync crons 20:30→18:00, 20:15→17:45
Запас ~3 часа до портального дедлайна 21:00 — эскалация на ярус 2/3
(медленный браузер / ручной оператор) происходит в рабочее время.
RefreshSupplierSessionJob daily — на 15 мин раньше sync (17:45).
Hourly RefreshSupplierSessionJob — без изменений.

Spec §4.7. Task 9 of 12. Tests 2/2 (cron expression + timezone).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:09 +03:00
Дмитрий 2f55632792 feat(supplier): wire jobs to FailoverProjectChannel
Оба job'а инжектят SupplierProjectChannel (DI → FailoverProjectChannel)
вместо прямого SupplierPortalClient. Catch TierEscalatedException +
WindowDeferredException — эскалация/перенос пропускают элемент, не валят job.

SyncSupplierProjectJob (singular): handle переписан — find-or-create local
supplier_projects row, portal-create через channel. ОТКЛОНЕНИЕ ОТ plan Step 8.1:
план писал channel-результат (portal external_id) прямо в projects.supplier_b*_
project_id, но эта колонка — FK на supplier_projects.id (local), не portal id.
Сохранена семантика ensureSupplierProject — job создаёт local row с
supplier_external_id и пишет в FK local id. ensureSupplierProject удалён из
SupplierPortalClient (был единственный consumer — этот job).

SyncSupplierProjectsJob (plural): handle/syncOne принимают channel; create →
createProjectForLiderra, update → updateProjectForLiderra (context-project из
liderraProjects->first() для project_id в очереди яруса 3).

Tests: singular переписан под SupplierProjectChannel mock (6 tests, incl.
idempotency reuse); plural — handle(AjaxProjectChannel) для non-failover
ветки (Http::fake-контракт сохранён). Larastan отложен на T12 (worktree
quirk — гонится в основной копии). Регрессия Pest 966/963/0 / 3 skipped.

Spec §5. Task 8 of 12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:08 +03:00
Дмитрий 54365015d8 feat(supplier): FormProjectChannel (Tier 2) + DI binding
PHP wrapper над manage-project.js через PlaywrightBridge.
+PlaywrightBridge::run(array): generic Node-скрипт runner (refreshSession
не тронут) — план Step 7.4 предусмотрел расширение bridge.
SupplierProjectChannel::class в DI резолвится в FailoverProjectChannel
(ярус 1 AjaxProjectChannel → ярус 2 FormProjectChannel → ярус 3 queue).

Spec §4.3, §4.4. Task 7 of 12. Channel-тесты 16/16 (Ajax 4 + Failover 7 + Form 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:08 +03:00
Дмитрий d760036972 feat(supplier): FailoverProjectChannel portal-side dedup before create
listProjects() матч по (platform, signal_type, unique_key) до create.
Защита от дубля при полу-успехе яруса 1 (create прошёл на портале, но
локальная запись не сохранилась → следующий запуск дублировал бы).
listProjects-сбой проглатывается — ярус-эскалация всё равно покроет.

Spec §4.4 шаг 2, §7. Task 5 of 12. Тесты 7/7 (19 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:07 +03:00
Дмитрий 0e27844a28 feat(supplier): FailoverProjectChannel skeleton — escalation matrix without dedup
Tier 1 → классификация исключения → ярус 2 (плейсхолдер) / ярус 3 (queue).
Без портального dedup (см. Task 5). Без реального Tier 2 (см. Task 7).

Матрица эскалации:
- Tier 1 success → return id
- WindowDeferredException → re-throw (операция переносится, без queue/alert)
- SupplierTransientException → сразу Tier 3 (skip Tier 2 — хост недоступен)
- SupplierClient/AuthException → Tier 2; success → failover_to_form alert;
  fail → Tier 3 queue + manual_required alert + TierEscalatedException
- escalateToTier3 пишет supplier_manual_sync_queue + queue'ит критический alert.

6 тестов матрицы эскалации зелёные (17 assertions). Spec §4.4, §6, §8.
Task 4 of 12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:06 +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
Дмитрий 54fcc4b094 feat(supplier): SupplierProjectChannel interface + AjaxProjectChannel (Tier 1)
Тонкий адаптер над SupplierPortalClient. Существующий клиент не меняется —
он остаётся HTTP-плумбингом, адаптер реализует интерфейс контракта.

Spec §4.1, §4.2. Task 2 of 12.

Tests: 4/4 passing (1 instanceof + 3 delegation: create/update/list).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:05 +03:00
Дмитрий e87b1385cf feat(supplier): verify rt-project-* contract live on crm.bp-gr.ru
Live discovery через Playwright MCP (Task 1):
- создан LIDPOTOK_TEST_DELETE_ME (B1+B2+B3) → 3 rt-проекта на портале;
- записаны сетевые запросы /admin/visit/rt-*;
- все три проекта удалены вручную, портал чист.

Endpoints (verified):
- POST /admin/visit/rt-project-save (create id:0, update id:N — same URL)
- POST /admin/visit/rt-project-delete (id строкой)
- GET  /admin/visit/rt-projects-load?src=none

Все три — application/json. Конверт ответа:
- success: HTTP 200 + {status:OK, message, result, id?:string}
- error:   HTTP 200 + {status:Error, message, result:null}
ID — строка (12721245), приводится к int (fits в int64).
Один save с B1+B2+B3 включёнными создаёт 3 rt-проекта — toPayload()
шлёт ровно один платформенный флаг (srcrt|srcbl|srcmt).

SupplierPortalClient:
- docblock переписан под verified контракт
- listProjects: путь /admin/visit/rt-projects-load + ?src=none query
- saveProject: путь /admin/visit/rt-project-save, asJson, парсинг id
- updateProject: тот же endpoint что save, id:N в body
- deleteProject: путь /admin/visit/rt-project-delete, asJson, id строкой
- new assertStatusOk() — HTTP 200 + status:Error → SupplierClientException
- toPayload(): полный Vuex-payload с маппингом DTO → portal:
  - platform B1/B2/B3 → srcrt/srcbl/srcmt (single-true)
  - signalType site/call/sms → type:hosts/calls/sms
  - workdays int[] → string[]
  - status active/paused → bool
  - + tag:_lidpotok, name/content из uniqueKey, defaults для show/depth/etc

Tests:
- new: tests/Feature/Supplier/SupplierPortalClientRtProjectTest.php (7 tests,
  contract: save+update+delete+list + 2 status:Error error-paths + B2/calls
  mapping)
- Sync/Cleanup/Unit тесты обновлены под новый URL + envelope shape.

Закрывает spec §1 honest-caveat «placeholder, не верифицирован»
и журнал решений запись 9. Регрессия: Pest 944/941/0 failed / 3 skipped
/ 2768 assertions / 59.2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:05 +03:00
Дмитрий 0f6f38a70e @
fix(supplier): реальные endpoint'ы отчёта «Запрос номеров» (discovery T3)

Discovery T3 на живом supplier-портале crm.bp-gr.ru (Playwright MCP)
вскрыл фактические endpoint'ы вместо placeholder'ов из spec §4.3:

- POST /admin/report/save-report (JSON body, selectType=49 + reportFilter)
  — возвращает строку "OK", не JSON с id;
- GET  /admin/report/load-reports — массив отчётов, id извлекается
  title-match'ем «Запрос номеров с {from} по {to}»;
- GET  /admin/report/getfile?id=N — 302 redirect на отдельный
  download-host (oki.needcallbuy.ru), Laravel HTTP follows redirect.

SupplierPortalClient: requestNumbersReport/waitReportReady/downloadReport
переписаны под реальный контракт; request() +параметр asJson;
connectTimeout(30)+timeout(60) против flaky DNS resolve.

refresh-session.js: селекторы login-формы Yii2 — placeholder
input[name=login] → реальные #loginform-username/-password.

Тесты SupplierPortalClientReportTest + CsvReconcileJobTest адаптированы
под новый внутренний контракт. Pest 15/15, Larastan 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-19 07:42:12 +03:00
Дмитрий 989ee58481 feat(supplier): API «Интеграция с поставщиком» — здоровье CSV-канала + ручная сверка 2026-05-18 17:51:44 +03:00
Дмитрий dd1f72bf58 feat(supplier): CsvReconcileJob — расписание каждые 30 минут 2026-05-18 17:46:01 +03:00
Дмитрий 0b6937973c feat(supplier): CsvReconcileJob — дедуп (phone,project) + async-флоу отчёта 2026-05-18 17:42:23 +03:00