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>
This commit is contained in:
Дмитрий
2026-05-17 10:05:32 +03:00
parent ec6ebc57e0
commit b1e903f31a
7 changed files with 48 additions and 7 deletions
@@ -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(),
@@ -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);
@@ -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]);
});
@@ -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]);
});
@@ -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');
});
+2 -6
View File
@@ -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();
+1 -1
View File
@@ -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 триггеров.