feat(helpdesk): add domain detail page with customer, contract, and handover sections

New domain detail page showing customer info, contract data, linked handovers,
updates section, and related domains for the same customer. Data loaded async
with skeleton loading states. Includes DomainDetailService and tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 13:21:24 +02:00
parent e7c60468c9
commit 693d33a0c8
6 changed files with 1074 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
<?php
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
use MintyPHP\Module\Helpdesk\Service\EffectiveHelpdeskSettingsService;
use PHPUnit\Framework\TestCase;
class DomainDetailServiceTest extends TestCase
{
private const TENANT_ID = 1;
private function createService(
?BcODataGateway $gateway = null,
?EffectiveHelpdeskSettingsService $settings = null,
?HandoverRepository $repository = null,
): DomainDetailService {
$gateway = $gateway ?? $this->createMock(BcODataGateway::class);
$settings = $settings ?? $this->createConfiguredSettings(true);
$repository = $repository ?? $this->createMock(HandoverRepository::class);
return new DomainDetailService($gateway, $settings, $repository);
}
private function createConfiguredSettings(bool $configured): EffectiveHelpdeskSettingsService
{
$mock = $this->createMock(EffectiveHelpdeskSettingsService::class);
$mock->method('isConfigured')->willReturn($configured);
return $mock;
}
// ── loadDomain tests ─────────────────────────────────────────
public function testLoadDomainHappyPath(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getDomain')->willReturn([
'No' => 'DNS00001',
'Customer_No' => 'D10001',
'Customer_Name' => 'Acme Corp',
'URL' => 'example.com',
'State' => 'Aktiv',
'Administration' => 'Intern',
]);
$service = $this->createService($gateway);
$result = $service->loadDomain('DNS00001');
$this->assertSame('success', $result['status']);
$this->assertSame('DNS00001', $result['domain']['No']);
$this->assertSame('example.com', $result['domain']['URL']);
}
public function testLoadDomainNotFound(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getDomain')->willReturn(null);
$service = $this->createService($gateway);
$result = $service->loadDomain('INVALID');
$this->assertSame('not_found', $result['status']);
}
public function testLoadDomainEmptyNo(): void
{
$service = $this->createService();
$result = $service->loadDomain('');
$this->assertSame('not_found', $result['status']);
}
public function testLoadDomainNotConfigured(): void
{
$service = $this->createService(settings: $this->createConfiguredSettings(false));
$result = $service->loadDomain('DNS00001');
$this->assertSame('not_configured', $result['status']);
}
public function testLoadDomainBcError(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getDomain')->willThrowException(new \RuntimeException('Connection failed'));
$service = $this->createService($gateway);
$result = $service->loadDomain('DNS00001');
$this->assertSame('error', $result['status']);
$this->assertSame('BC connection failed', $result['error']);
}
// ── loadDomainDetails tests ──────────────────────────────────
public function testLoadDomainDetailsHappyPath(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getCustomer')->willReturn([
'No' => 'D10001',
'Name' => 'Acme Corp',
'Address' => 'Main St 1',
]);
$gateway->method('listDomainContractLines')->willReturn([
['No' => 'DNS00001', 'Header_No' => 'V1000', 'PI_Header_Type' => 'WEBSITE', 'PI_Header_Description' => 'Website Contract', 'PI_Header_State' => 'Aktiv', 'Line_State' => 'Aktiv'],
['No' => 'DNS00002', 'Header_No' => 'V2000', 'PI_Header_Type' => 'SW&H', 'PI_Header_Description' => 'Other', 'PI_Header_State' => 'Aktiv', 'Line_State' => 'Aktiv'],
]);
$gateway->method('getDomainsForCustomer')->willReturn([
['No' => 'DNS00001', 'URL' => 'example.com', 'State' => 'Aktiv'],
['No' => 'DNS00002', 'URL' => 'other.com', 'State' => 'Aktiv'],
]);
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findByDomainNo')->willReturn([
['id' => 1, 'product_code' => 'PROD-A', 'product_name' => 'Product A', 'product_bc_description' => '', 'status' => 'draft', 'created_by_name' => 'John', 'created_at' => '2026-01-01 10:00:00'],
['id' => 2, 'product_code' => 'PROD-A', 'product_name' => 'Product A', 'product_bc_description' => '', 'status' => 'completed', 'created_by_name' => 'Jane', 'created_at' => '2026-02-15 14:00:00'],
]);
$service = $this->createService($gateway, repository: $repo);
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
$this->assertTrue($result['ok']);
$this->assertSame('Acme Corp', $result['customer']['Name']);
$this->assertSame('WEBSITE', $result['contract']['contract_type']);
$this->assertSame('V1000', $result['contract']['contract_no']);
$this->assertCount(2, $result['handovers']);
$this->assertSame(2, $result['handover_stats']['total']);
$this->assertSame(1, $result['handover_stats']['draft']);
$this->assertSame(1, $result['handover_stats']['completed']);
$this->assertCount(1, $result['related_domains']);
$this->assertSame('DNS00002', $result['related_domains'][0]['No']);
}
public function testLoadDomainDetailsNoContract(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getCustomer')->willReturn(['No' => 'D10001', 'Name' => 'Acme']);
$gateway->method('listDomainContractLines')->willReturn([]);
$gateway->method('getDomainsForCustomer')->willReturn([]);
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findByDomainNo')->willReturn([]);
$service = $this->createService($gateway, repository: $repo);
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
$this->assertTrue($result['ok']);
$this->assertNull($result['contract']);
$this->assertSame(0, $result['handover_stats']['total']);
$this->assertEmpty($result['related_domains']);
}
public function testLoadDomainDetailsEmptyDomainNo(): void
{
$service = $this->createService();
$result = $service->loadDomainDetails('', 'D10001', self::TENANT_ID);
$this->assertFalse($result['ok']);
$this->assertSame('Missing domain number', $result['error']);
}
public function testLoadDomainDetailsCustomerFetchFails(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getCustomer')->willThrowException(new \RuntimeException('BC error'));
$gateway->method('listDomainContractLines')->willReturn([]);
$gateway->method('getDomainsForCustomer')->willReturn([]);
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findByDomainNo')->willReturn([]);
$service = $this->createService($gateway, repository: $repo);
$result = $service->loadDomainDetails('DNS00001', 'D10001', self::TENANT_ID);
$this->assertTrue($result['ok']);
$this->assertNull($result['customer']);
}
public function testLoadDomainDetailsHandoverStatAggregation(): void
{
$gateway = $this->createMock(BcODataGateway::class);
$gateway->method('getCustomer')->willReturn(null);
$gateway->method('listDomainContractLines')->willReturn([]);
$gateway->method('getDomainsForCustomer')->willReturn([]);
$repo = $this->createMock(HandoverRepository::class);
$repo->method('findByDomainNo')->willReturn([
['id' => 1, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'draft', 'created_by_name' => '', 'created_at' => ''],
['id' => 2, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'in_progress', 'created_by_name' => '', 'created_at' => ''],
['id' => 3, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'in_progress', 'created_by_name' => '', 'created_at' => ''],
['id' => 4, 'product_code' => 'P', 'product_name' => '', 'product_bc_description' => '', 'status' => 'archived', 'created_by_name' => '', 'created_at' => ''],
]);
$service = $this->createService($gateway, repository: $repo);
$result = $service->loadDomainDetails('DNS00001', '', self::TENANT_ID);
$this->assertSame(4, $result['handover_stats']['total']);
$this->assertSame(1, $result['handover_stats']['draft']);
$this->assertSame(2, $result['handover_stats']['in_progress']);
$this->assertSame(0, $result['handover_stats']['completed']);
$this->assertSame(1, $result['handover_stats']['archived']);
}
}