tenant = Tenant::factory()->create(); $this->otherTenant = Tenant::factory()->create(); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); $this->project = Project::factory()->for($this->tenant)->create(['name' => 'Окна Москва']); $this->project2 = Project::factory()->for($this->tenant)->create(['name' => 'Натяжные потолки']); $this->manager = User::factory()->for($this->tenant)->create([ 'first_name' => 'Иван', 'last_name' => 'Петров', 'email' => 'ivan@example.test', ]); }); test('GET /api/deals возвращает 422 без tenant_id', function () { $this->getJson('/api/deals')->assertStatus(422); }); test('GET /api/deals возвращает 404 для unknown tenant_id', function () { $this->getJson('/api/deals?tenant_id=999999')->assertStatus(404); }); test('GET /api/deals возвращает пустой список для tenant без сделок', function () { $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); $r->assertStatus(200) ->assertJson(['deals' => [], 'total' => 0, 'limit' => 100, 'offset' => 0]); }); test('GET /api/deals возвращает сделки tenant\'а с проектом и менеджером', function () { $deal = Deal::factory() ->for($this->tenant) ->for($this->project) ->create([ 'phone' => '+7 (999) 111-11-11', 'contact_name' => 'Анна С.', 'status' => 'new', 'manager_id' => $this->manager->id, ]); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); $r->assertStatus(200); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.id'))->toBe($deal->id); expect($r->json('deals.0.phone'))->toBe('+7 (999) 111-11-11'); expect($r->json('deals.0.project_name'))->toBe('Окна Москва'); expect($r->json('deals.0.manager_name'))->toBe('Иван П.'); expect($r->json('deals.0.manager_initials'))->toBe('ИП'); expect($r->json('deals.0.contact_name'))->toBe('Анна С.'); expect($r->json('deals.0.received_at'))->toBeString(); }); test('GET /api/deals не возвращает сделки чужого tenant\'а (RLS)', function () { Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']); DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id); $foreignProject = Project::factory()->for($this->otherTenant)->create(); Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(['status' => 'new']); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.tenant_id'))->toBe($this->tenant->id); }); test('GET /api/deals сортирует по received_at DESC', function () { $oldest = Deal::factory()->for($this->tenant)->for($this->project)->create([ 'received_at' => now()->subHours(3), ]); $newest = Deal::factory()->for($this->tenant)->for($this->project)->create([ 'received_at' => now()->subMinutes(1), ]); $middle = Deal::factory()->for($this->tenant)->for($this->project)->create([ 'received_at' => now()->subHours(1), ]); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); expect($r->json('deals.0.id'))->toBe($newest->id); expect($r->json('deals.1.id'))->toBe($middle->id); expect($r->json('deals.2.id'))->toBe($oldest->id); }); test('GET /api/deals фильтрует по status_in[]', function () { Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'new']); Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'paid']); Deal::factory()->for($this->tenant)->for($this->project)->create(['status' => 'closed']); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&status_in[]=new&status_in[]=paid'); expect($r->json('total'))->toBe(2); $statuses = collect($r->json('deals'))->pluck('status')->sort()->values()->all(); expect($statuses)->toBe(['new', 'paid']); }); test('GET /api/deals фильтрует по project_id', function () { Deal::factory()->for($this->tenant)->for($this->project)->create(); Deal::factory()->for($this->tenant)->for($this->project)->create(); Deal::factory()->for($this->tenant)->for($this->project2)->create(); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&project_id='.$this->project2->id); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.project_name'))->toBe('Натяжные потолки'); }); test('GET /api/deals фильтрует по manager_id', function () { $other = User::factory()->for($this->tenant)->create(); Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $this->manager->id]); Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => $other->id]); Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&manager_id='.$this->manager->id); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.manager_id'))->toBe($this->manager->id); }); test('GET /api/deals фильтрует по search (phone + contact_name, ILIKE)', function () { Deal::factory()->for($this->tenant)->for($this->project)->create([ 'phone' => '+7 (999) 111-11-11', 'contact_name' => 'Анна Соколова', ]); Deal::factory()->for($this->tenant)->for($this->project)->create([ 'phone' => '+7 (903) 222-22-22', 'contact_name' => 'Дмитрий Петров', ]); expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=Соколова') ->json('total'))->toBe(1); expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=903') ->json('total'))->toBe(1); expect($this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&search=сокол') // case-insensitive ILIKE ->json('total'))->toBe(1); }); test('GET /api/deals поддерживает limit + offset', function () { foreach (range(1, 5) as $i) { Deal::factory()->for($this->tenant)->for($this->project)->create([ 'received_at' => now()->subMinutes($i), ]); } $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&limit=2&offset=1'); expect($r->json('total'))->toBe(5); expect($r->json('limit'))->toBe(2); expect($r->json('offset'))->toBe(1); expect(count($r->json('deals')))->toBe(2); }); test('GET /api/deals?only_deleted=true возвращает только soft-deleted', function () { $alive = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted1 = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted2 = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted1->delete(); $deleted2->delete(); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&only_deleted=true'); expect($r->json('total'))->toBe(2); $ids = collect($r->json('deals'))->pluck('id')->all(); expect($ids)->not->toContain($alive->id); expect($ids)->toContain($deleted1->id); expect($ids)->toContain($deleted2->id); }); test('GET /api/deals (без only_deleted) НЕ возвращает soft-deleted (default)', function () { $alive = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted = Deal::factory()->for($this->tenant)->for($this->project)->create(); $deleted->delete(); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.id'))->toBe($alive->id); }); test('GET /api/deals?only_deleted=true изолирует чужие удалённые сделки', function () { DB::statement('SET app.current_tenant_id = '.$this->otherTenant->id); $foreignProject = Project::factory()->for($this->otherTenant)->create(); $foreign = Deal::factory()->for($this->otherTenant)->for($foreignProject)->create(); $foreign->delete(); DB::statement('SET app.current_tenant_id = '.$this->tenant->id); $own = Deal::factory()->for($this->tenant)->for($this->project)->create(); $own->delete(); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id.'&only_deleted=true'); expect($r->json('total'))->toBe(1); expect($r->json('deals.0.id'))->toBe($own->id); }); test('GET /api/deals возвращает manager_name/initials = null если manager_id null', function () { Deal::factory()->for($this->tenant)->for($this->project)->create(['manager_id' => null]); $r = $this->getJson('/api/deals?tenant_id='.$this->tenant->id); expect($r->json('deals.0.manager_id'))->toBeNull(); expect($r->json('deals.0.manager_name'))->toBeNull(); expect($r->json('deals.0.manager_initials'))->toBeNull(); });