From b1e903f31a01c4802ca601377946dfdaf9897d70 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: Sun, 17 May 2026 10:05:32 +0300 Subject: [PATCH] =?UTF-8?q?fix(projects):=20C9=20code-review=20findings=20?= =?UTF-8?q?=E2=80=94=20ProjectResource=20=D0=BE=D1=82=D0=B4=D0=B0=D1=91?= =?UTF-8?q?=D1=82=20regions[]=20+=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/app/Http/Resources/ProjectResource.php | 1 + app/app/Services/Project/ProjectService.php | 7 ++++ .../Plan5/Projects/ProjectsStoreTest.php | 1 + .../Plan5/Projects/ProjectsUpdateTest.php | 1 + .../Supplier/SyncSupplierProjectsJobTest.php | 35 +++++++++++++++++++ app/tests/Frontend/NewProjectDialog.spec.ts | 8 ++--- db/CHANGELOG_schema.md | 2 +- 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/app/Http/Resources/ProjectResource.php b/app/app/Http/Resources/ProjectResource.php index 88ab4db6..1d7fc46d 100644 --- a/app/app/Http/Resources/ProjectResource.php +++ b/app/app/Http/Resources/ProjectResource.php @@ -31,6 +31,7 @@ class ProjectResource extends JsonResource 'archived_at' => $project->archived_at?->toIso8601String(), 'region_mask' => $this->region_mask, 'region_mode' => $this->region_mode, + 'regions' => $this->regions, 'delivery_days_mask' => $this->delivery_days_mask, 'sync_status' => $this->aggregateSyncStatus(), 'last_synced_at' => $this->aggregateLastSyncedAt(), diff --git a/app/app/Services/Project/ProjectService.php b/app/app/Services/Project/ProjectService.php index fd5f39f6..78278c55 100644 --- a/app/app/Services/Project/ProjectService.php +++ b/app/app/Services/Project/ProjectService.php @@ -114,6 +114,13 @@ class ProjectService return ['updated' => $updated, 'skipped' => [], 'warnings' => []]; } + /** + * LEGACY (Plan 6): обновляет только bitmask `region_mask` федеральных округов. + * После Plan 6 источник истины региональной фильтрации — `regions` INT[]; + * outbound SyncSupplierProjectsJob читает `regions[]`, НЕ `region_mask`. Значит + * этот bulk-action на реальную фильтрацию у поставщика не влияет. Субъект-уровневый + * bulk-edit `regions[]` запланирован в Plan 6.5 (spec §13 — out of scope C9). + */ private function bulkUpdateRegions($query, array $payload): array { $add = (int) ($payload['add'] ?? 0); diff --git a/app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php b/app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php index 9baf9db9..96165116 100644 --- a/app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php +++ b/app/tests/Feature/Plan5/Projects/ProjectsStoreTest.php @@ -160,6 +160,7 @@ it('creates project with subject-level regions array', function () { ]); $response->assertStatus(201); + $response->assertJsonPath('data.regions', [82, 83]); $created = Project::where('name', 'Regions Test Project')->firstOrFail(); expect($created->regions)->toBe([82, 83]); }); diff --git a/app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php b/app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php index 2398ef7c..d0544f68 100644 --- a/app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php +++ b/app/tests/Feature/Plan5/Projects/ProjectsUpdateTest.php @@ -106,6 +106,7 @@ it('updates regions array via PATCH', function () { ]); $response->assertStatus(200); + $response->assertJsonPath('data.regions', [82]); expect($project->fresh()->regions)->toBe([82]); }); diff --git a/app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php b/app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php index 4d8d29e8..adf1b36a 100644 --- a/app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php +++ b/app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php @@ -377,3 +377,38 @@ test('sticky auth error throws and sends critical alert email', function (): voi return $mail->alertType === 'sticky_auth'; }); }); + +test('outbound: copies project regions[] into supplier_project current_regions via full handle()', function (): void { + $tenant = Tenant::factory()->create(); + $sp = SupplierProject::factory()->create([ + 'platform' => 'B1', + 'signal_type' => 'site', + 'unique_key' => 'regions-flow.example.com', + 'supplier_external_id' => null, + 'current_limit' => 0, + 'current_workdays' => [], + 'current_regions' => [], + ]); + Project::factory()->create([ + 'tenant_id' => $tenant->id, + 'is_active' => true, + 'signal_type' => 'site', + 'signal_identifier' => 'regions-flow.example.com', + 'supplier_b1_project_id' => $sp->id, + 'daily_limit_target' => 9, + 'delivery_days_mask' => 127, + 'regions' => [82, 83], + 'region_mask' => 255, + 'region_mode' => 'include', + ]); + + Http::fake([ + 'crm.bp-gr.ru/admin/rt-project-save' => Http::response(['id' => 556], 200), + ]); + + (new SyncSupplierProjectsJob)->handle(); + + $sp->refresh(); + expect($sp->current_regions)->toBe([82, 83]) + ->and($sp->supplier_external_id)->toBe('556'); +}); diff --git a/app/tests/Frontend/NewProjectDialog.spec.ts b/app/tests/Frontend/NewProjectDialog.spec.ts index fe83abaa..835e3b7f 100644 --- a/app/tests/Frontend/NewProjectDialog.spec.ts +++ b/app/tests/Frontend/NewProjectDialog.spec.ts @@ -94,15 +94,11 @@ describe('NewProjectDialog', () => { expect((autocomplete.props('items') as Array<{ code: number }>).every((r) => r.code !== 0)).toBe(true); }); - it.skip('sends regions array in POST payload', async () => { - // TODO: submit() requires Vuetify form rendering + filling signal_identifier, - // name, daily_limit_target — JSDOM нестабильно для Vuetify v-tabs/v-text-field. - // Covered visually через Plan 6.5 visual smoke + e2e (Playwright). - // Critical guarantee — items count test (#1) выше остаётся обязательным. + it('sends regions array in POST payload', async () => { const wrapper = factory(); await flushPromises(); const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' }); - await autocomplete.vm.$emit('update:model-value', [82, 83]); + autocomplete.vm.$emit('update:model-value', [82, 83]); await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); diff --git a/db/CHANGELOG_schema.md b/db/CHANGELOG_schema.md index 25ab7116..600ffbf3 100644 --- a/db/CHANGELOG_schema.md +++ b/db/CHANGELOG_schema.md @@ -23,7 +23,7 @@ **Семантика:** - `regions=[]` → «вся РФ» (паритет с legacy `region_mask=255 + region_mode='include'`) -- `regions=[77,82]` → проект принимает лиды только из Москвы (77) и Чукотки (82) +- `regions=[82,83]` → проект принимает лиды только из Москвы (82) и Санкт-Петербурга (83) **Schema baseline после v8.22:** 64 базовых таблиц / 12 партиций / **119 индексов** (+1 GIN) / 40 RLS / 5 функций / 13 триггеров.