*/ private static function ticket( string $no, string $customerNo, string $customerName, int $createdDaysAgo, ?int $lastActivityDaysAgo = null ): array { $createdTs = self::NOW_TS - ($createdDaysAgo * 86400); $activityTs = $lastActivityDaysAgo !== null ? self::NOW_TS - ($lastActivityDaysAgo * 86400) : $createdTs; return [ 'No' => $no, 'Customer_No' => $customerNo, 'Cust_Name' => $customerName, 'Created_On' => gmdate('Y-m-d\TH:i:s\Z', $createdTs), 'Last_Activity_Date' => gmdate('Y-m-d\TH:i:s\Z', $activityTs), 'Ticket_State' => 'Offen', ]; } public function testEmptyTicketsReturnsEmptyResult(): void { $result = CustomerEngagementService::buildEngagement([], 90, 60, self::NOW_TS); $this->assertSame(0, $result['summary']['total_customers']); $this->assertSame(0, $result['summary']['active']); $this->assertSame(0, $result['summary']['silent']); $this->assertSame([], $result['silent']); $this->assertSame([], $result['top_communication']); } public function testSkipsTicketsWithoutCustomerNo(): void { $tickets = [ self::ticket('T1', '', 'No Customer', 5), self::ticket('T2', '10610', 'Valid', 5), ]; $result = CustomerEngagementService::buildEngagement($tickets, 90, 60, self::NOW_TS); $this->assertSame(1, $result['summary']['total_customers']); } public function testAggregatesTicketCountsPerCustomer(): void { $tickets = [ self::ticket('T1', '10610', 'Kunde A', 5), self::ticket('T2', '10610', 'Kunde A', 10), self::ticket('T3', '10620', 'Kunde B', 3), ]; $result = CustomerEngagementService::buildEngagement($tickets, 90, 60, self::NOW_TS); $this->assertSame(2, $result['summary']['total_customers']); $top = $result['top_communication']; $byCustomer = []; foreach ($top as $row) { $byCustomer[$row['customer_no']] = $row; } $this->assertSame(2, $byCustomer['10610']['ticket_count_period']); $this->assertSame(1, $byCustomer['10620']['ticket_count_period']); } public function testActiveCustomerWithinThreshold(): void { // Last contact 10 days ago, threshold 60 => active. $tickets = [self::ticket('T1', '10610', 'Kunde A', 10)]; $result = CustomerEngagementService::buildEngagement($tickets, 90, 60, self::NOW_TS); $this->assertSame(1, $result['summary']['active']); $this->assertSame(0, $result['summary']['silent']); $this->assertSame([], $result['silent']); } public function testSilentCustomerBeyondThreshold(): void { // Last contact 90 days ago, threshold 60 => silent. $tickets = [self::ticket('T1', '10610', 'Kunde A', 90)]; $result = CustomerEngagementService::buildEngagement($tickets, 365, 60, self::NOW_TS); $this->assertSame(0, $result['summary']['active']); $this->assertSame(1, $result['summary']['silent']); $this->assertCount(1, $result['silent']); $this->assertSame('10610', $result['silent'][0]['customer_no']); $this->assertSame('silent', $result['silent'][0]['status']); $this->assertSame(90, $result['silent'][0]['days_since_contact']); } public function testLastContactUsesMostRecentActivity(): void { // Two tickets: one created 100 days ago but updated 5 days ago, one created 50 days ago. // Most recent activity is 5 days ago => active. $tickets = [ self::ticket('T1', '10610', 'Kunde A', 100, 5), self::ticket('T2', '10610', 'Kunde A', 50, 50), ]; $result = CustomerEngagementService::buildEngagement($tickets, 365, 60, self::NOW_TS); $this->assertSame(1, $result['summary']['active']); $row = $result['top_communication'][0]; $this->assertSame(5, $row['days_since_contact']); } public function testSilentListSortedByLongestSilenceFirst(): void { $tickets = [ self::ticket('T1', '10610', 'Recently Silent', 70), self::ticket('T2', '10620', 'Long Silent', 200), self::ticket('T3', '10630', 'Mid Silent', 120), ]; $result = CustomerEngagementService::buildEngagement($tickets, 365, 60, self::NOW_TS); $order = array_column($result['silent'], 'customer_no'); $this->assertSame(['10620', '10630', '10610'], $order); } public function testTopCommunicationExcludesCustomersWithNoPeriodActivity(): void { // Ticket older than the 30-day period => not counted in top list. $tickets = [ self::ticket('T1', '10610', 'Active', 5), self::ticket('T2', '10620', 'Old', 100), ]; $result = CustomerEngagementService::buildEngagement($tickets, 30, 60, self::NOW_TS); $tops = array_column($result['top_communication'], 'customer_no'); $this->assertContains('10610', $tops); $this->assertNotContains('10620', $tops); } public function testIgnoresUninitializedBcDates(): void { // BC sentinel date 0001-01-01 must be treated as "no contact". $tickets = [[ 'No' => 'T1', 'Customer_No' => '10610', 'Cust_Name' => 'Kunde A', 'Created_On' => '0001-01-01T00:00:00Z', 'Last_Activity_Date' => '0001-01-01T00:00:00Z', 'Ticket_State' => 'Offen', ]]; $result = CustomerEngagementService::buildEngagement($tickets, 90, 60, self::NOW_TS); $this->assertSame(1, $result['summary']['no_contact_ever']); $this->assertNull($result['silent'][0]['last_contact_ts']); $this->assertSame('no_contact_ever', $result['silent'][0]['status']); } public function testMetaReflectsInputs(): void { $result = CustomerEngagementService::buildEngagement([], 180, 45, self::NOW_TS, true); $this->assertSame(180, $result['meta']['period_days']); $this->assertSame(45, $result['meta']['inactivity_days']); $this->assertTrue($result['meta']['truncated']); $this->assertSame('medium', $result['meta']['confidence']); $this->assertSame(self::NOW_TS, $result['meta']['generated_ts']); } }