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:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 триггеров.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user