515114cff5
3 интеграционных доработки после backend-completion v1.57.
(1) GET /api/managers + /api/projects + manager FK guard:
- ManagerController::index — active users тенанта (is_active+deleted_at IS NULL).
Формат {id, email, first_name, last_name, name, initials} с
formatName/formatInitials helpers (fallback на email).
- ProjectController::index — active projects (is_active=true).
- Оба endpoint'а: tenant_id query-param, 422 без, 404 unknown, RLS-обёртка.
- DealController::store FK guard: manager_id должен принадлежать tenant'у +
is_active. Иначе 422 (закрывает security-gap чужого менеджера).
- Pest +8 в LookupsTest.
(2) Replace MOCK_MANAGERS / MOCK_PROJECTS на API в NewDealDialog:
- projectOptions/managerOptions ref'ы с MOCK fallback.
- loadLookups через Promise.all([listProjects, listManagers]) на open
диалога с tenantId.
- managerIdByName Map name→id для submit'а.
- Silent fallback на mock при network-error.
- Vitest +2.
(3) SupplierLeadCost для manual-leads:
- В DealController::store после Deal::create — resolveSupplierId (копия
логики ProcessWebhookJob: project_suppliers JOIN suppliers + ORDER BY
sort_order). Если supplier найден — SupplierLeadCost с snapshot cost_rub
+ supplier_lead_id=NULL (manual: нет внешнего id).
- Manual по-прежнему НЕ списывает баланс (Ю-2 reseller-модель — charge
только при webhook'е); cost-аналитика всё равно нужна.
- Pest +2.
- TODO: рефактор resolveSupplierId в App\Services\SupplierResolver чтобы
Job + Controller разделяли логику.
Старый тест manager_id=42 переписан под FK guard через User::factory.
PHPStan baseline регенерирован (+28 ignored Pest TestCall warnings).
Регресс: lint+type-check+format ✅; vitest 247/247 за 16.32 сек (+2);
vite build 951 ms; Pint+PHPStan passed; Pest 166/166 за 22.11 сек
(+10 от 156, 699 assertions). Реестр v1.57→v1.58, CLAUDE.md v1.48→v1.49.
Production TODO остаточные:
- resolveSupplierId → SupplierResolver service.
- XLSX-export через PhpSpreadsheet.
- GET /api/deals для replace MOCK_DEALS в DealsView/KanbanView.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
4.4 KiB
PHP
123 lines
4.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
|
||
beforeEach(function () {
|
||
$this->tenant = Tenant::factory()->create();
|
||
});
|
||
|
||
test('GET /api/managers возвращает active users тенанта', function () {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
User::factory()->for($this->tenant)->create([
|
||
'first_name' => 'Иван', 'last_name' => 'Петров', 'is_active' => true,
|
||
]);
|
||
User::factory()->for($this->tenant)->create([
|
||
'first_name' => 'Ольга', 'last_name' => 'Романова', 'is_active' => true,
|
||
]);
|
||
User::factory()->for($this->tenant)->create([
|
||
'first_name' => 'Удалённый', 'is_active' => false,
|
||
]);
|
||
|
||
$r = $this->getJson('/api/managers?tenant_id='.$this->tenant->id);
|
||
$r->assertStatus(200);
|
||
$managers = $r->json('managers');
|
||
expect($managers)->toHaveCount(2);
|
||
$names = array_column($managers, 'name');
|
||
expect($names)->toContain('Иван П.')->toContain('Ольга Р.');
|
||
});
|
||
|
||
test('GET /api/managers возвращает initials с fallback на email', function () {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
User::factory()->for($this->tenant)->create([
|
||
'email' => 'admin@example.ru',
|
||
'first_name' => null,
|
||
'last_name' => null,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
$r = $this->getJson('/api/managers?tenant_id='.$this->tenant->id);
|
||
$r->assertStatus(200);
|
||
$manager = $r->json('managers.0');
|
||
expect($manager['name'])->toBe('admin@example.ru');
|
||
expect($manager['initials'])->toBe('AD');
|
||
});
|
||
|
||
test('GET /api/managers 422 без tenant_id', function () {
|
||
$r = $this->getJson('/api/managers');
|
||
$r->assertStatus(422);
|
||
});
|
||
|
||
test('GET /api/managers 404 unknown tenant', function () {
|
||
$r = $this->getJson('/api/managers?tenant_id=999999');
|
||
$r->assertStatus(404);
|
||
});
|
||
|
||
test('GET /api/projects возвращает active projects тенанта', function () {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
Project::create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'name' => 'Окна Москва', 'is_active' => true,
|
||
]);
|
||
Project::create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'name' => 'Архивный', 'is_active' => false,
|
||
]);
|
||
|
||
$r = $this->getJson('/api/projects?tenant_id='.$this->tenant->id);
|
||
$r->assertStatus(200);
|
||
$projects = $r->json('projects');
|
||
expect($projects)->toHaveCount(1);
|
||
expect($projects[0]['name'])->toBe('Окна Москва');
|
||
});
|
||
|
||
test('POST /api/deals 422 если manager_id не принадлежит tenant\'у', function () {
|
||
$otherTenant = Tenant::factory()->create();
|
||
DB::statement('SET app.current_tenant_id = '.$otherTenant->id);
|
||
$otherManager = User::factory()->for($otherTenant)->create(['is_active' => true]);
|
||
|
||
// Назначаем чужого менеджера на свою сделку — должен быть 422.
|
||
$r = $this->postJson('/api/deals', [
|
||
'tenant_id' => $this->tenant->id,
|
||
'project_name' => 'X',
|
||
'phone' => '+7 (999) 000-00-00',
|
||
'manager_id' => $otherManager->id,
|
||
]);
|
||
$r->assertStatus(422);
|
||
expect($r->json('errors'))->toHaveKey('manager_id');
|
||
});
|
||
|
||
test('POST /api/deals 422 если manager_id не активен (is_active=false)', function () {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
$inactive = User::factory()->for($this->tenant)->create(['is_active' => false]);
|
||
|
||
$r = $this->postJson('/api/deals', [
|
||
'tenant_id' => $this->tenant->id,
|
||
'project_name' => 'X',
|
||
'phone' => '+7 (999) 000-00-00',
|
||
'manager_id' => $inactive->id,
|
||
]);
|
||
$r->assertStatus(422);
|
||
});
|
||
|
||
test('POST /api/deals принимает manager_id из своего tenant\'а', function () {
|
||
DB::statement('SET app.current_tenant_id = '.$this->tenant->id);
|
||
$manager = User::factory()->for($this->tenant)->create(['is_active' => true]);
|
||
|
||
$r = $this->postJson('/api/deals', [
|
||
'tenant_id' => $this->tenant->id,
|
||
'project_name' => 'X',
|
||
'phone' => '+7 (999) 000-00-00',
|
||
'manager_id' => $manager->id,
|
||
]);
|
||
$r->assertStatus(201);
|
||
expect($r->json('deal.manager_id'))->toBe($manager->id);
|
||
});
|