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 триггеров.