tenant = Tenant::factory()->create(['balance_leads' => 100]); }); function makeCall(array $over = []): array { return array_merge([ 'name' => 'Проект A', 'signal_type' => 'call', 'signal_identifier' => '79991110000', 'daily_limit_target' => 5, 'regions' => [], 'delivery_days_mask' => 31, ], $over); } it('blocks duplicate source within tenant with human message', function () { app(ProjectService::class)->create($this->tenant, makeCall()); expect(fn () => app(ProjectService::class) ->create($this->tenant, makeCall(['name' => 'Проект B']))) ->toThrow(HttpResponseException::class); }); it('allows same source for a different tenant (sharing)', function () { $other = Tenant::factory()->create(['balance_leads' => 100]); app(ProjectService::class)->create($this->tenant, makeCall()); $p = app(ProjectService::class)->create($other, makeCall(['name' => 'Проект B'])); expect($p)->toBeInstanceOf(Project::class); }); it('blocks duplicate name within tenant with human message (not SQL)', function () { app(ProjectService::class)->create($this->tenant, makeCall()); try { app(ProjectService::class) ->create($this->tenant, makeCall(['name' => 'Проект A', 'signal_identifier' => '79992220000'])); $this->fail('expected HttpResponseException'); } catch (HttpResponseException $e) { $body = $e->getResponse()->getData(true); expect($body['errors']['name'][0] ?? '')->not->toContain('SQLSTATE'); } });