Commit Graph

206 Commits

Author SHA1 Message Date
Дмитрий 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
Дмитрий 3e70f87d88 feat(supplier): SupplierPortalClient — async-флоу заказа отчёта «Запрос номеров» 2026-05-18 17:36:27 +03:00
Дмитрий 7e8560ae58 feat(supplier): SupplierCsvParser под отчёт «Запрос номеров» (Name;Tag;Phone) 2026-05-18 17:26:53 +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
Дмитрий e3ef9d70be fix(supplier): parseProjectField извлекает встроенный домен из имени B1-проекта
Поставщик crm.bp-gr.ru шлёт B1-проекты, чьё имя — свободный текст со
встроенным URL/доменом (B1_заявка carmoney.ru/, B1_Платежи
cabinet.caranga.ru/login, B1_krk-finance.ru/cabinet/auth). Старый
anchored-regex требовал, чтобы вся строка после B1_ была чистым доменом;
такой rest не матчил — классификация sms — B1+sms — DomainException
(chk_supplier_projects_b1_not_for_sms) — 21 реальный лид застрял с error,
0 сделок.

Fix: после двух anchored-проверок (call/site) — fallback-извлечение
домена с латинским TLD из любой позиции строки — signal_type=site,
identifier = извлечённый домен. Реальные sms-имена (B1_TINKOFF) без
точки-домена остаются sms — существующий B1+SMS-тест не затронут.

3 параметризованных теста (carmoney/caranga/krk) + регрессия:
RouteSupplierLeadJobTest 12/12, Supplier+Integration+Webhook 61/61.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:31:45 +03:00
Дмитрий f248e27702 feat(projects/drawer): редактирование «Источника» (site/call/sms) в карточке проекта
UX-request 18.05.2026 (п.9):
- ProjectDetailsDrawer (правая панель на /projects) теперь редактирует
  signal_identifier для site (домен) и call (телефон 7\d{10}); для sms —
  sms_senders+sms_keyword (как раньше).
- Поле «Источник» отображается **только** в карточке проекта (read-only
  в drawer сделки на /deals — Task 2 закрыл).

Backend:
- UpdateProjectRequest: condition-based валидация по signal_type из БД
  (site domain regex, call 11-digit 7\d{10}; sms — без новых правил)
- ProjectService::update: убран signal_identifier из silent-drop;
  $needsResync расширен на signal_identifier → SyncSupplierProjectJob

signal_type остаётся immutable (менять тип проекта — отдельная задача).

Larastan baseline bumped (ProjectsUpdateTest: actingAs 8→12 для 4 новых тестов).
Pest tests/Feature/Plan5/Projects/ProjectsUpdateTest 12/12.
Vitest 33 passes на Project-spec'ах. Build 2.03s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:44:03 +03:00
Дмитрий 789e7dcdb6 feat(deals/drawer): убрать «Менеджер», добавить «Тип» + «Источник» read-only
UX-request 18.05.2026 (пп.4/6/7):
- удалена секция «Менеджер»/«Не назначен» (менеджеров в системе пока нет)
- добавлен параметр «Тип» (Сайт/Звонок/СМС) — project.signal_type
- добавлен параметр «Источник» (read-only):
  - site/call → project.signal_identifier (домен или телефон)
  - sms → sms_senders[0] + ' (KEYWORD)' если sms_keyword не пустой
- удалён hardcoded «Я.Директ → landing-1»

Backend: DealController index + show + update payload расширены 4 полями
project_signal_type/identifier/sms_keyword/sms_senders + eager-load
project relation расширен.

Редактирование источника — только в карточке проекта (Task 5 плана).

Larastan baseline bumped (DealShowTest: tenant 13→20, getJson 7→10 для 3 новых тестов).
Pest 51/51 на Deal-endpoints.
Vitest 108 files / 875 passed / 3 skipped (5 новых тестов DealDetailBody).
Build 2.30s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:24:57 +03:00
Дмитрий b4138bbc82 feat(deals): sweep 14->5 funnel slugs — controllers, mocks, stories, tests 2026-05-18 03:42:41 +03:00
Дмитрий 77cc535ab2 feat(deals): migration — remap deals.status + drop obsolete lead_statuses (14->5) 2026-05-18 03:42:41 +03:00
Дмитрий e9ae43a81b test(deals): drop obsolete ids-based export tests from DealCreateTest (superseded by DealExportTest)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 03:42:40 +03:00
Дмитрий f98a3bf109 feat(deals): DealExportController -- export by delivery-date range, lead-registry columns 2026-05-18 03:42:40 +03:00
Дмитрий 3981fdcbf3 fix(deals): DealController@index — 422 on malformed received_from/received_to date params 2026-05-18 03:42:40 +03:00
Дмитрий 5234e46d92 feat(deals): DealController@index — received_at date-range filter + comment/city/signal_type/next_reminder_at 2026-05-18 03:42:40 +03:00
Дмитрий 54451d2ea6 feat(projects): RegionsBulkDialog — subject-level regions (89 RF subjects) #1426
Bulk regions dialog reworked from federal-district bitmask to subject/region
selection, consistent with ProjectDetailsDrawer/NewProjectDialog. Full-stack:
add_regions/remove_regions on projects.regions INT[], BulkProjectActionRequest
split validation, ProjectService model-instance update. federal-districts.ts
removed (zero consumers). +menuRepositionFix util for v-autocomplete menu.
phpstan-baseline: bump actingAs ignore count 14->15 (new validation test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 03:41:46 +03:00
Дмитрий b1e903f31a fix(projects): C9 code-review findings — ProjectResource отдаёт regions[] + покрытие
C1: ProjectResource не возвращал regions → edit-диалог/drawer затирали
    сохранённые регионы при сохранении. +поле в toArray().
C2: +integration-тест outbound regions[] через полный SyncSupplierProjectsJob::handle().
I1: расскип NewProjectDialog payload-теста (regions в POST).
I2: assert data.regions в ProjectsStore/UpdateTest (ловит C1 на backend-уровне).
I4: docblock — bulkUpdateRegions legacy (region_mask, не влияет на outbound до Plan 6.5).
M1: CHANGELOG v8.22 — исправлен неверный пример регионов (Москва=82).

Регрессия: Pest 905/902/3sk/0, Vitest 104f/884/3sk/0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:05:32 +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
Дмитрий 21d84a77a9 style(admin): Sprint 5C — pint-fix AdminPricingTiersControllerTest 2026-05-17 05:24:44 +03:00
Дмитрий 2172d2ba45 fix(admin): G7 review-fixup — сброс effective_from при открытии редактора + boundary-тест 2026-05-17 05:24:44 +03:00
Дмитрий 9f791f9f93 feat(admin): G7 — выбор effective_from тарифной сетки через date-picker 2026-05-17 05:24:44 +03:00