createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => true, 'soap_used' => true, 'return_value' => 0, 'communication_text' => "2026-04-03 08:10 - Alice: Hallo\n2026-04-03 08:11 - Alice: Update", 'message_entries' => '', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([ [ 'Entry_No' => 1, 'Created_By' => 'ICTEST', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '07:59:00.000', 'Type' => 'Status', 'State_Subtype' => 'Offen', 'Record_ID' => 'ICI Support Ticket: T26-1216', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1216', 40); $this->assertTrue($result['ok']); $this->assertIsArray($result['entries']); $this->assertGreaterThanOrEqual(3, count($result['entries'])); $this->assertSame(true, $result['meta']['soap_used']); $this->assertSame(false, $result['meta']['fallback_used']); $sources = array_unique(array_map(static fn (array $entry): string => (string) ($entry['source'] ?? ''), $result['entries'])); $this->assertContains('soap', $sources); $this->assertContains('odata', $sources); } public function testLoadTicketCommunicationFeedFallsBackToODataWhenSoapFails(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => false, 'soap_used' => false, 'return_value' => 400, 'communication_text' => '', 'message_entries' => '', 'error' => 'BC SOAP returned error code 400', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([ [ 'Entry_No' => 1, 'Created_By' => 'ICTEST', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '07:59:00.000', 'Type' => 'Nachricht', 'State_Subtype' => '', 'Record_ID' => 'Fallback entry', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1216', 40); $this->assertTrue($result['ok']); $this->assertNotEmpty($result['entries']); $this->assertSame(false, $result['meta']['soap_used']); $this->assertSame(true, $result['meta']['fallback_used']); $this->assertSame('BC SOAP returned error code 400', $result['meta']['soap_error']); $this->assertSame('odata', $result['entries'][0]['source']); } public function testLoadDebitorCommunicationFeedUsesLastTenTickets(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => false, 'soap_used' => false, 'return_value' => 400, 'communication_text' => '', 'message_entries' => '', 'error' => 'SOAP unavailable', ]); $tickets = []; for ($i = 1; $i <= 12; $i++) { $tickets[] = [ 'No' => 'T' . $i, 'Last_Activity_Date' => sprintf('2026-04-%02dT08:00:00Z', $i), 'Created_On' => sprintf('2026-03-%02dT08:00:00Z', $i), ]; } $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn($tickets); $odataGateway->expects($this->any())->method('getContactsForCustomer')->with('', 'Test GmbH')->willReturn([]); $odataGateway->method('getTicketLog')->willReturnCallback(static function (string $ticketNo): array { return [[ 'Entry_No' => 1, 'Created_By' => 'ICTEST', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '07:59:00.000', 'Type' => 'Nachricht', 'State_Subtype' => '', 'Record_ID' => 'Entry ' . $ticketNo, ]]; }); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadDebitorCommunicationFeed('10001', 'Test GmbH', 10, 40, 250); $this->assertTrue($result['ok']); $this->assertSame(10, $result['meta']['tickets_considered']); $this->assertSame(10, $result['meta']['tickets_with_data']); $this->assertSame(10, $result['meta']['soap_partial_failures']); $ticketNos = array_unique(array_map(static fn (array $entry): string => (string) ($entry['ticket_no'] ?? ''), $result['entries'])); $this->assertCount(10, $ticketNos); $this->assertNotContains('T1', $ticketNos); $this->assertNotContains('T2', $ticketNos); $this->assertContains('T12', $ticketNos); } public function testLoadDebitorCommunicationFeedResolvesContactActorsOncePerDebitor(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => false, 'soap_used' => false, 'return_value' => 400, 'communication_text' => '', 'message_entries' => '', 'error' => 'SOAP unavailable', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn([ ['No' => 'T1', 'Last_Activity_Date' => '2026-04-10T08:00:00Z', 'Created_On' => '2026-04-09T08:00:00Z'], ['No' => 'T2', 'Last_Activity_Date' => '2026-04-09T08:00:00Z', 'Created_On' => '2026-04-08T08:00:00Z'], ]); $odataGateway->expects($this->once()) ->method('getContactsForCustomer') ->with('', 'Test GmbH') ->willReturn([ ['No' => 'KT002967', 'Name' => 'Felix Schneider'], ]); $odataGateway->expects($this->never()) ->method('getTicket'); $odataGateway->method('getTicketLog')->willReturn([ [ 'Entry_No' => 1, 'Created_By' => 'KT002967', 'Created_By_Type' => 'Contact', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '07:59:00.000', 'Type' => 'Nachricht', 'State_Subtype' => '', 'Record_ID' => 'Hallo', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadDebitorCommunicationFeed('10001', 'Test GmbH', 2, 40, 250); $this->assertTrue($result['ok']); $this->assertNotEmpty($result['entries']); $actors = array_unique(array_map(static fn (array $entry): string => (string) ($entry['actor'] ?? ''), $result['entries'])); $this->assertSame(['Felix Schneider'], $actors); } public function testLoadTicketCommunicationFeedRespectsEntryLimit(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => true, 'soap_used' => true, 'return_value' => 0, 'communication_text' => "2026-04-03 08:10 - Alice: A\n2026-04-03 08:11 - Alice: B\n2026-04-03 08:12 - Alice: C", 'message_entries' => '', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1216', 2); $this->assertTrue($result['ok']); $this->assertCount(2, $result['entries']); } public function testLoadTicketCommunicationFeedMapsSoapDataBlobByEntryNo(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => true, 'soap_used' => true, 'return_value' => 100, 'communication_text' => '[{"EntryNo":"19356","DataBlob":"

Hallo Team

"}]', 'message_entries' => '', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->method('getTicketLog')->willReturn([ [ 'Entry_No' => 19356, 'Created_By' => 'KT002452', 'Creation_Date' => '2026-04-02', 'Creation_Time' => '13:54:00.000', 'Type' => 'Nachricht', 'State_Subtype' => '', 'Record_ID' => '', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1204', 40); $this->assertTrue($result['ok']); $this->assertNotEmpty($result['entries']); $this->assertSame(false, $result['meta']['fallback_used']); $this->assertSame('soap', $result['entries'][0]['source']); $this->assertSame('Hallo Team', $result['entries'][0]['message']); } public function testLoadTicketCommunicationFeedResolvesContactCodeToName(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => false, 'soap_used' => false, 'return_value' => 400, 'communication_text' => '', 'message_entries' => '', 'error' => 'BC SOAP returned error code 400', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1204')->willReturn([ [ 'Entry_No' => 19356, 'Created_By' => 'KT002967', 'Creation_Date' => '2026-04-02', 'Creation_Time' => '13:54:00.000', 'Type' => 'Nachricht', 'State_Subtype' => '', 'Record_ID' => 'Hallo', ], ]); $odataGateway->expects($this->any())->method('getTicket')->with('T26-1204')->willReturn([ 'No' => 'T26-1204', 'Company_Contact_Name' => 'Aero-Dienst GmbH', 'Current_Contact_Name' => 'Fallback Kontakt', ]); $odataGateway->expects($this->any())->method('getContactsForCustomer')->with('', 'Aero-Dienst GmbH')->willReturn([ [ 'No' => 'KT002967', 'Name' => 'Felix Schneider', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1204', 40); $this->assertTrue($result['ok']); $this->assertNotEmpty($result['entries']); $this->assertSame('Felix Schneider', $result['entries'][0]['actor']); } public function testFileTypeEntriesIncludeFileMetadata(): void { $soapGateway = $this->createMock(BcSoapGateway::class); $soapGateway->method('getTicketCommunicationText')->willReturn([ 'ok' => false, 'soap_used' => false, 'return_value' => 0, 'communication_text' => '', 'message_entries' => '', ]); $odataGateway = $this->createMock(BcODataGateway::class); $odataGateway->method('getTicketLog')->willReturn([ [ 'Entry_No' => 42, 'Created_By' => 'NKS', 'Created_By_Type' => 'Benutzer', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '10:00:00.000', 'Type' => 'Datei', 'State_Subtype' => '', 'Record_ID' => 'screenshot.png', ], [ 'Entry_No' => 41, 'Created_By' => 'NKS', 'Created_By_Type' => 'Benutzer', 'Creation_Date' => '2026-04-03', 'Creation_Time' => '09:55:00.000', 'Type' => 'Status', 'State_Subtype' => 'Offen', 'Record_ID' => 'ICI Support Ticket: T26-1216', ], ]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1216', 40); $this->assertTrue($result['ok']); $entries = $result['entries']; // Find the file entry $fileEntries = array_values(array_filter($entries, static fn (array $e) => ($e['type_variant'] ?? '') === 'file')); $this->assertNotEmpty($fileEntries, 'Should have at least one file entry'); $fileEntry = $fileEntries[0]; $this->assertSame('42', $fileEntry['file_entry_no']); $this->assertSame('screenshot.png', $fileEntry['file_name']); // Non-file entries should have empty file fields $statusEntries = array_values(array_filter($entries, static fn (array $e) => ($e['type_variant'] ?? '') === 'status')); if ($statusEntries !== []) { $this->assertSame('', $statusEntries[0]['file_entry_no']); $this->assertSame('', $statusEntries[0]['file_name']); } } }