From f888c4baec64dec6c54fa2af6a783c96a96eade2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 7 Jul 2026 16:03:43 +0300 Subject: [PATCH] =?UTF-8?q?chore(autopodbor):=20=D0=BA=D0=BD=D0=BE=D0=BF?= =?UTF-8?q?=D0=BA=D0=B0=20=C2=AB=D0=9E=D1=82=D0=BA=D0=B0=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=D1=81=D1=8F=C2=BB=20(=D0=B1=D1=8B=D0=BB=D0=BE=20=C2=AB?= =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=C2=BB)=20+=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D0=BF=D0=BE=D0=B4=D0=B1=D0=BE=D1=80=20=D0=B2?= =?UTF-8?q?=20CSV=20=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B9=20+?= =?UTF-8?q?=20canon-sync=20schema.sql=20v8.63?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Кнопка на актуализации переименована «Удалить»→«Отказаться» (чтобы клиент не путал с удалением конкурента). - CSV-выгрузка «Списаний» теперь включает автоподбор-строки (отдельным блоком, «Автоподбор конкурентов»); тест на export. charges 11/11. - schema.sql canon-sync v8.63: +колонка dismissed_actualize_keys у autopodbor_competitors + CHANGELOG (заодно в цепочку шапки вписана пропущенная v8.62 CHECK autopodbor_charge). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Api/TenantChargesController.php | 23 +++++++++++++++++++ .../screens/FieldProposalsScreen.vue | 2 +- .../Billing/TenantChargesControllerTest.php | 16 +++++++++++++ db/CHANGELOG_schema.md | 17 +++++++++++++- db/schema.sql | 3 ++- 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/app/app/Http/Controllers/Api/TenantChargesController.php b/app/app/Http/Controllers/Api/TenantChargesController.php index 4e1a546c..25aca45b 100644 --- a/app/app/Http/Controllers/Api/TenantChargesController.php +++ b/app/app/Http/Controllers/Api/TenantChargesController.php @@ -161,6 +161,29 @@ class TenantChargesController extends Controller } }); + // Списания автоподбора — тоже в CSV (после лид-строк). charge_source к ним неприменим, + // поэтому при выбранном источнике их не выгружаем (как и в списке). + if ($source === null || $source === '') { + $autoq = DB::table('balance_transactions') + ->where('tenant_id', $tenantId) + ->where('type', 'autopodbor_charge') + ->orderByDesc('created_at') + ->orderByDesc('id'); + $this->applyPeriodTo($autoq, 'created_at', $period); + $autoq->chunk(500, function ($rows) use ($out) { + foreach ($rows as $r) { + fputcsv($out, [ + Carbon::parse($r->created_at)->toIso8601String(), + 'Автоподбор конкурентов', + '', + '', + number_format(abs((float) $r->amount_rub), 2, '.', ''), + $r->balance_rub_after ?? '', + ]); + } + }); + } + fclose($out); }); }, 200, [ diff --git a/app/resources/js/views/autopodbor/screens/FieldProposalsScreen.vue b/app/resources/js/views/autopodbor/screens/FieldProposalsScreen.vue index 3a960f74..6f8fdf75 100644 --- a/app/resources/js/views/autopodbor/screens/FieldProposalsScreen.vue +++ b/app/resources/js/views/autopodbor/screens/FieldProposalsScreen.vue @@ -367,7 +367,7 @@ defineExpose({ list, selected, toggle, toggleAll, moveToField, moveSelected, ope - + diff --git a/app/tests/Feature/Billing/TenantChargesControllerTest.php b/app/tests/Feature/Billing/TenantChargesControllerTest.php index c7ce3dee..7e625cdc 100644 --- a/app/tests/Feature/Billing/TenantChargesControllerTest.php +++ b/app/tests/Feature/Billing/TenantChargesControllerTest.php @@ -205,3 +205,19 @@ it('не подмешивает автоподбор при фильтре charg expect($data)->toHaveCount(1) ->and($data->firstWhere('kind', 'autopodbor'))->toBeNull(); }); + +it('export включает списания автоподбора отдельными строками', function () { + makeChargeFor($this->tenant); + BalanceTransaction::factory()->create([ + 'tenant_id' => $this->tenant->id, + 'type' => 'autopodbor_charge', + 'amount_rub' => '-50.00', + 'balance_rub_after' => '100.00', + 'created_at' => now(), + ]); + + $body = $this->postJson('/api/billing/charges/export')->assertOk()->streamedContent(); + + expect($body)->toContain('Автоподбор конкурентов') + ->and($body)->toContain('50.00'); +}); diff --git a/db/CHANGELOG_schema.md b/db/CHANGELOG_schema.md index 5b6bdb4e..621868e5 100644 --- a/db/CHANGELOG_schema.md +++ b/db/CHANGELOG_schema.md @@ -2,7 +2,22 @@ **Назначение:** консолидированный журнал изменений `schema.sql`. Содержит тридцать записей в обратном хронологическом порядке (v8.33 → v8.32 → v8.31 → v8.30 → v8.29 → v8.28 → v8.27 → v8.26 → v8.25 → v8.24 → v8.23 → v8.22 → v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog. -**Файл схемы:** `schema.sql` (текущая версия — v8.62, консолидированная — разворачивает БД с нуля). +**Файл схемы:** `schema.sql` (текущая версия — v8.63, консолидированная — разворачивает БД с нуля). + +## v8.63 (2026-07-07) — Автоподбор: колонка dismissed_actualize_keys (мягкое «Отказаться» на актуализации) + +Ветка `worktree-avtopodbor`. Кнопка «Отказаться» на карточках «На актуализацию»: клиент отклоняет +находку нового сайта/адреса — фирма поля остаётся, находка уходит в архив, а её новые ключи-опознавалки +запоминаются у фирмы поля. `ProposalClassifier` вычитает отклонённые ключи из дельты → та же находка +больше не всплывает («не засорять ленту»); реально другой новый сайт даёт новый ключ → снова покажется. + +**Свёрнуто в тело:** `autopodbor_competitors.dismissed_actualize_keys JSONB NOT NULL DEFAULT '[]'::jsonb` +(`schema.sql` ~л.1415). Миграция `2026_07_07_130000_add_dismissed_actualize_keys_to_autopodbor_competitors` +идемпотентна (`Schema::hasColumn`), migrate:fresh зелёный, ветка автоподбора 185/185. + +**Структурно:** `autopodbor_competitors` +1 колонка. Счётчики таблиц/индексов/RLS/функций/триггеров/партиций +БЕЗ изменений (nullable/default ADD COLUMN, RLS построчная не меняется). **Колонка добавлена на прод +миграцией 07.07** (ролью `crm_migrator`); в остальном фича воркстри-only. ## v8.62 (2026-07-06) — Автоподбор: canon-sync денежной таблицы (пропущенный CHECK) diff --git a/db/schema.sql b/db/schema.sql index 797d3a67..4e3ee827 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,7 +1,7 @@ -- ============================================================================= -- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра») -- Версия: v8.62 (06.07.2026 — Автоподбор canon-sync ДЕНЕЖНОЙ таблицы: в CHECK balance_transactions_type_check добавлен тип 'autopodbor_charge' (списание за прогон автоподбора, AutopodborChargeService; миграция 2026_06_28_110100 — ранее НЕ была свёрнута в тело DDL, нашёл rls-reviewer при подготовке боевого выката). Структурно меняется только значение CHECK — счётчики таблиц/индексов/RLS/функций/триггеров/партиций БЕЗ изменений. Воркстри-фича avtopodbor, НЕ на проде. Прежняя версия ниже.) --- Базовая версия: v8.61 (05.07.2026 — Автоподбор, закрытие отложенного «хвоста» + журнал слияний. Свёрнуты в тело DDL ранее отложенные инкременты: autopodbor_competitors +box VARCHAR(16) DEFAULT 'proposal'+chk (proposal|field|archived, миграции 2026_06_29_120000/2026_07_01_100000) +индекс autopodbor_competitors_tenant_box_idx (tenant_id,box) [это и был «⏸ ХВОСТ» из v8.60] +phones JSONB DEFAULT '[]' (2026_07_03_120000) +elements JSONB (2026_07_04_140000); autopodbor_runs +progress JSONB +result JSONB (2026_07_05_120000/130000). НОВАЯ таблица autopodbor_merge_events (журнал слияний конкурентов + снимок поглощённых карточек absorbed для истории/возврата; per-tenant RLS ENABLE+FORCE tenant_isolation; FK user_id→users SET NULL, survivor_id→autopodbor_competitors SET NULL; индекс (tenant_id,created_at); миграция 2026_07_05_140000). ALTER-миграции автоподбора идемпотентны (elements/runs_progress/runs_result доguard-ены Schema::hasColumn; add_box/add_phones/add_phone_type/add_where_found — hasColumn; create — to_regclass): migrate:fresh зелёный end-to-end, весь блок автоподбора 428/428, squawk 0, rls-reviewer OK. Структурно: autopodbor_competitors +3 колонки +1 индекс +1 CHECK; autopodbor_runs +2 колонки; +1 regular-таблица autopodbor_merge_events (+1 индекс +1 RLS-политика). Воркстри-фича avtopodbor, НЕ на проде. Функций/триггеров/партиций без изменений. Прежняя v8.60, 01.07.2026 — Автоподбор: box += 'archived' у autopodbor_sources CHECK (мягкое удаление конкурента + архив старого сайта при актуализации), миграция 2026_07_01_100000; тот же 'archived' в DB у autopodbor_competitors.box — но его CHECK в этом DDL по-прежнему отложенный хвост, см. ниже. Прежняя v8.59, 30.06.2026 — Автоподбор, богатый провенанс источника + canon-sync инкрементов 29.06: autopodbor_sources +3 колонки where_found JSONB (список «где нашли» [{label,url}], подтверждения = длина) / office VARCHAR(255) (адрес филиала из карточки справочника) / confirmations SMALLINT DEFAULT 1 (сортировка «больше подтверждений — выше»), миграция 2026_06_30_120000 (идемпотентная ADD COLUMN, nullable/default — старые строки не переписываются, RLS построчная не меняется). В ТОТ ЖЕ DDL внесены ранее не синканные инкременты 29.06: phone_type VARCHAR(12)+chk (миграция 2026_06_29_120100), box VARCHAR(16) DEFAULT 'proposal'+chk+индекс autopodbor_sources_competitor_box_idx (миграция 2026_06_29_120000). Структурно для autopodbor_sources: +5 колонок (3 новых + 2 catch-up), +1 индекс, +2 CHECK. ⏸ ХВОСТ: box у autopodbor_competitors (+колонка+chk+индекс autopodbor_competitors_tenant_box_idx, та же миграция 2026_06_29_120000) в DDL ещё НЕ синкан — отдельный canon-sync; точная пересверка счётчиков таблиц/индексов/RLS отложена (прецедент v8.54/v8.55). Воркстри-фича avtopodbor, НЕ на проде. RLS/функций/триггеров/партиций без изменений.) +-- Базовая версия: v8.63 (07.07.2026 — Автоподбор: +колонка dismissed_actualize_keys JSONB NOT NULL DEFAULT '[]' у autopodbor_competitors — отклонённые клиентом ключи актуализации (кнопка «Отказаться»: находка нового сайта/адреса уходит, её ключи запоминаются у фирмы поля, ProposalClassifier их вычитает → та же находка больше не всплывает; реально другой новый сайт даст новый ключ → покажется). Миграция 2026_07_07_130000 идемпотентна (Schema::hasColumn), migrate:fresh зелёный, ветка автоподбора 185/185. Структурно: autopodbor_competitors +1 колонка. Воркстри-фича avtopodbor, НЕ на проде (колонка добавлена на прод миграцией 07.07). RLS/индексов/функций/триггеров/партиций без изменений.) Прежняя v8.62 (06.07.2026 — canon-sync денежной таблицы: balance_transactions.type CHECK += 'autopodbor_charge', пропущенный инкремент миграции 2026_06_28_110100). Прежняя v8.61 (05.07.2026 — Автоподбор, закрытие отложенного «хвоста» + журнал слияний. Свёрнуты в тело DDL ранее отложенные инкременты: autopodbor_competitors +box VARCHAR(16) DEFAULT 'proposal'+chk (proposal|field|archived, миграции 2026_06_29_120000/2026_07_01_100000) +индекс autopodbor_competitors_tenant_box_idx (tenant_id,box) [это и был «⏸ ХВОСТ» из v8.60] +phones JSONB DEFAULT '[]' (2026_07_03_120000) +elements JSONB (2026_07_04_140000); autopodbor_runs +progress JSONB +result JSONB (2026_07_05_120000/130000). НОВАЯ таблица autopodbor_merge_events (журнал слияний конкурентов + снимок поглощённых карточек absorbed для истории/возврата; per-tenant RLS ENABLE+FORCE tenant_isolation; FK user_id→users SET NULL, survivor_id→autopodbor_competitors SET NULL; индекс (tenant_id,created_at); миграция 2026_07_05_140000). ALTER-миграции автоподбора идемпотентны (elements/runs_progress/runs_result доguard-ены Schema::hasColumn; add_box/add_phones/add_phone_type/add_where_found — hasColumn; create — to_regclass): migrate:fresh зелёный end-to-end, весь блок автоподбора 428/428, squawk 0, rls-reviewer OK. Структурно: autopodbor_competitors +3 колонки +1 индекс +1 CHECK; autopodbor_runs +2 колонки; +1 regular-таблица autopodbor_merge_events (+1 индекс +1 RLS-политика). Воркстри-фича avtopodbor, НЕ на проде. Функций/триггеров/партиций без изменений. Прежняя v8.60, 01.07.2026 — Автоподбор: box += 'archived' у autopodbor_sources CHECK (мягкое удаление конкурента + архив старого сайта при актуализации), миграция 2026_07_01_100000; тот же 'archived' в DB у autopodbor_competitors.box — но его CHECK в этом DDL по-прежнему отложенный хвост, см. ниже. Прежняя v8.59, 30.06.2026 — Автоподбор, богатый провенанс источника + canon-sync инкрементов 29.06: autopodbor_sources +3 колонки where_found JSONB (список «где нашли» [{label,url}], подтверждения = длина) / office VARCHAR(255) (адрес филиала из карточки справочника) / confirmations SMALLINT DEFAULT 1 (сортировка «больше подтверждений — выше»), миграция 2026_06_30_120000 (идемпотентная ADD COLUMN, nullable/default — старые строки не переписываются, RLS построчная не меняется). В ТОТ ЖЕ DDL внесены ранее не синканные инкременты 29.06: phone_type VARCHAR(12)+chk (миграция 2026_06_29_120100), box VARCHAR(16) DEFAULT 'proposal'+chk+индекс autopodbor_sources_competitor_box_idx (миграция 2026_06_29_120000). Структурно для autopodbor_sources: +5 колонок (3 новых + 2 catch-up), +1 индекс, +2 CHECK. ⏸ ХВОСТ: box у autopodbor_competitors (+колонка+chk+индекс autopodbor_competitors_tenant_box_idx, та же миграция 2026_06_29_120000) в DDL ещё НЕ синкан — отдельный canon-sync; точная пересверка счётчиков таблиц/индексов/RLS отложена (прецедент v8.54/v8.55). Воркстри-фича avtopodbor, НЕ на проде. RLS/функций/триггеров/партиций без изменений.) -- Базовая версия: v8.58 (28.06.2026 — Автоподбор конкурентов: +3 regular-таблицы autopodbor_runs/competitors/sources, RLS tenant_isolation в харднинг-форме (ENABLE+FORCE+NULLIF), +6 индексов (вкл. 2 UNIQUE dedup: autopodbor_competitor_dedup, autopodbor_source_dedup), +3 RLS-политики. Счётчики: таблиц 79→82 (regular 69→72) / индексов 124→130 / RLS 44→47. Миграции 2026_06_28_100000/100100/100200. Функций/триггеров/партиций без изменений. GRANTs через blanket ON ALL TABLES в db/02_grants.sql — отдельных строк не добавлено.) -- Базовая версия: v8.57 (26.06.2026 — RLS GUC hardening: ВСЕ 44 политики tenant_isolation приведены к NULLIF(current_setting('app.current_tenant_id', true), '')::bigint — устранён класс отказов на Managed PG/PgBouncer, когда GUC app.current_tenant_id пуст ('' → 22P02) либо не задан (→ 42704). Инцидент 26.06: вход в портал падал на резолве users (60 ошибок, все на users). 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes, user_sessions) дополнительно получили разрешающую ветку «NULLIF(...) IS NULL OR ...» — читаются/пишутся ДО tenant-контекста на auth-роутах без 'tenant' middleware; при ЗАДАННОМ tenant изоляция НЕ меняется (rls-reviewer APPROVE). Структурно БД НЕ меняется — переписаны только USING/WITH CHECK (счётчики таблиц/индексов/RLS=44/функций/триггеров без изменений). Миграция 2026_06_26_153000_rls_nullif_guc_hardening (идемпотентна). Применена на боевой кластер; lead_charges FORCE RLS сохранён.) -- Базовая версия: v8.56 (26.06.2026 — Путь А, шов C: тело функции audit_block_mutation() пропускает пересчёт hash-цепочки по метке GUC app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО session_replication_role (superuser-only, недоступен в Yandex Managed PG). Append-only сохранён: без метки UPDATE/DELETE аудита запрещён. Структурно БД НЕ меняется — только тело функции (счётчики таблиц/индексов/RLS/функций/триггеров без изменений, функций по-прежнему 5). Миграция 2026_06_26_140000_audit_block_mutation_guc_rebuild_flag. AuditRebuildChain команда переведена на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe).) @@ -1412,6 +1412,7 @@ CREATE TABLE autopodbor_competitors ( box VARCHAR(16) NOT NULL DEFAULT 'proposal', -- proposal | field | archived (миграции 2026_06_29_120000 / 2026_07_01_100000) phones JSONB NOT NULL DEFAULT '[]'::jsonb, -- нормализованные телефоны, вскрытые при сборе (миграция 2026_07_03_120000) elements JSONB, -- подкомпании/ЖК: [{name,sites,cards}] (миграция 2026_07_04_140000) + dismissed_actualize_keys JSONB NOT NULL DEFAULT '[]'::jsonb, -- отклонённые клиентом ключи актуализации (миграция 2026_07_07_130000) CONSTRAINT autopodbor_competitors_box_chk CHECK (box IN ('proposal', 'field', 'archived')) ); CREATE INDEX ON autopodbor_competitors (tenant_id, search_run_id);