>, * meta?: array, * error?: string * } */ public function loadTicketCommunicationFeed( string $ticketNo, int $maxEntries = 40, array $actorNameMap = [], bool $allowPerTicketActorLookup = true ): array { $ticketNo = trim($ticketNo); if ($ticketNo === '') { return ['ok' => false, 'error' => 'Missing ticket number']; } $maxEntries = max(1, min(250, $maxEntries)); $ticketLog = []; $odataError = ''; $odataEntries = []; try { $ticketLog = $this->bcODataGateway->getTicketLog($ticketNo); } catch (\Throwable $e) { $odataError = $e->getMessage(); } $messageEntries = $this->buildSoapMessageEntriesFromTicketLog($ticketLog); $soapResult = $this->bcSoapGateway->getTicketCommunicationText($ticketNo, $messageEntries); $soapError = (string) ($soapResult['error'] ?? ''); $soapUsed = (bool) $soapResult['soap_used']; $soapEntries = []; $soapMessageMap = []; if ($soapResult['ok'] === true) { $soapEntries = $this->parseSoapEntries( $ticketNo, (string) $soapResult['communication_text'], (string) $soapResult['message_entries'] ); $soapMessageMap = $this->extractSoapMessageMap( (string) $soapResult['communication_text'], (string) $soapResult['message_entries'] ); } if ($ticketLog !== []) { $odataEntries = $this->mapODataEntries($ticketNo, $ticketLog, $soapMessageMap); } // When OData entries are enriched with SOAP message text via soapMessageMap, // the separate SOAP entries for those messages are redundant and cause // duplicates. Drop SOAP message entries when the map was applied. if ($soapMessageMap !== [] && $soapEntries !== []) { $soapEntries = array_values(array_filter( $soapEntries, static fn (array $e): bool => ($e['type_variant'] ?? '') !== 'message' )); } if ($actorNameMap !== []) { $odataEntries = $this->applyActorNameMap($odataEntries, $actorNameMap); $soapEntries = $this->applyActorNameMap($soapEntries, $actorNameMap); } elseif ($allowPerTicketActorLookup) { $ticket = null; try { $ticket = $this->bcODataGateway->getTicket($ticketNo); } catch (\Throwable) { // ignore } $resolvedActorNameMap = $this->resolveContactActorNames($ticket, $odataEntries, $soapEntries); $supportActorNameMap = $this->resolveSupportActorNames($ticket, $odataEntries, $soapEntries); $combinedMap = array_merge($resolvedActorNameMap, $supportActorNameMap); if ($combinedMap !== []) { $odataEntries = $this->applyActorNameMap($odataEntries, $combinedMap); $soapEntries = $this->applyActorNameMap($soapEntries, $combinedMap); } } $merged = $this->mergeEntries($odataEntries, $soapEntries, $maxEntries); $fallbackUsed = $this->hasFallbackOnlyEntries($odataEntries) && $soapEntries === []; if ($merged === [] && $soapError !== '' && $odataError !== '') { return [ 'ok' => false, 'error' => 'Could not load communication feed', 'meta' => [ 'ticket_no' => $ticketNo, 'soap_used' => false, 'soap_error' => $soapError, 'fallback_used' => false, ], ]; } return [ 'ok' => true, 'entries' => $merged, 'meta' => [ 'ticket_no' => $ticketNo, 'soap_used' => $soapUsed, 'soap_error' => $soapError, 'fallback_used' => $fallbackUsed, ], ]; } /** * @return array{ * ok: bool, * entries?: array>, * meta?: array, * error?: string * } */ public function loadDebitorCommunicationFeed( string $customerNo, string $customerName, int $ticketsLimit = 10, int $maxEntriesPerTicket = 40, int $maxEntriesTotal = 250 ): array { $customerNo = trim($customerNo); $customerName = trim($customerName); if ($customerNo === '' || $customerName === '') { return ['ok' => false, 'error' => 'Missing customer number or name']; } $ticketsLimit = max(1, min(25, $ticketsLimit)); $maxEntriesPerTicket = max(1, min(80, $maxEntriesPerTicket)); $maxEntriesTotal = max(1, min(500, $maxEntriesTotal)); try { $tickets = $this->bcODataGateway->getTicketsForCustomer($customerNo, $customerName); } catch (\Throwable) { return ['ok' => false, 'error' => 'Could not load tickets for customer']; } if ($tickets === []) { return [ 'ok' => true, 'entries' => [], 'meta' => [ 'customer_no' => $customerNo, 'tickets_considered' => 0, 'tickets_with_data' => 0, 'soap_partial_failures' => 0, ], ]; } usort($tickets, function (array $a, array $b): int { $sa = $this->ticketSortStamp($a); $sb = $this->ticketSortStamp($b); return $sb <=> $sa; }); $ticketsToProcess = []; foreach ($tickets as $ticket) { $ticketNo = trim((string) ($ticket['No'] ?? '')); if ($ticketNo === '') { continue; } $ticketsToProcess[$ticketNo] = true; if (count($ticketsToProcess) >= $ticketsLimit) { break; } } $allEntries = []; $ticketsWithData = 0; $soapPartialFailures = 0; $actorNameMap = $this->buildContactActorNameMapForDebitor($customerName); // Collect support user name → ticket code mappings $supportUserNamesByTicket = []; foreach ($tickets as $ticket) { if (!is_array($ticket)) { continue; } $no = trim((string) ($ticket['No'] ?? '')); $name = trim((string) ($ticket['Support_User_Name'] ?? '')); if ($no !== '' && $name !== '') { $supportUserNamesByTicket[$no] = $name; } } foreach (array_keys($ticketsToProcess) as $ticketNo) { $feed = $this->loadTicketCommunicationFeed( $ticketNo, $maxEntriesPerTicket, $actorNameMap, false ); $ticketEntries = []; if ($feed['ok']) { $ticketEntries = is_array($feed['entries'] ?? null) ? $feed['entries'] : []; } if ($ticketEntries !== []) { $ticketsWithData++; foreach ($ticketEntries as $entry) { $allEntries[] = $entry; } } $meta = is_array($feed['meta'] ?? null) ? $feed['meta'] : []; $soapUsed = (bool) ($meta['soap_used'] ?? false); $soapError = trim((string) ($meta['soap_error'] ?? '')); if (!$soapUsed && $soapError !== '') { $soapPartialFailures++; } } // Build support code→name map from entries + ticket data, then apply $supportActorNameMap = $this->buildSupportActorNameMapFromEntries($allEntries, $supportUserNamesByTicket); if ($supportActorNameMap !== []) { $allEntries = $this->applyActorNameMap($allEntries, $supportActorNameMap); } $entries = $this->mergeEntries($allEntries, [], $maxEntriesTotal); return [ 'ok' => true, 'entries' => $entries, 'meta' => [ 'customer_no' => $customerNo, 'tickets_considered' => count($ticketsToProcess), 'tickets_with_data' => $ticketsWithData, 'soap_partial_failures' => $soapPartialFailures, ], ]; } /** * @param array> $ticketLog * @param array $soapMessageMap * @return array> */ private function mapODataEntries(string $ticketNo, array $ticketLog, array $soapMessageMap = []): array { $entries = []; foreach ($ticketLog as $row) { $type = trim((string) ($row['Type'] ?? '')); $stateSubtype = trim((string) ($row['State_Subtype'] ?? '')); $recordId = trim((string) ($row['Record_ID'] ?? '')); $actor = trim((string) ($row['Created_By'] ?? '')); $createdByType = trim((string) ($row['Created_By_Type'] ?? '')); $date = trim((string) ($row['Creation_Date'] ?? '')); $time = trim((string) ($row['Creation_Time'] ?? '')); $entryNo = $this->normalizeEntryNo($row['Entry_No'] ?? null); $timestampIso = $this->combineDateTimeToIso($date, $time); $soapMessage = $entryNo !== '' ? (string) ($soapMessageMap[$entryNo] ?? '') : ''; $message = $soapMessage !== '' ? $soapMessage : $this->buildODataMessage($type, $stateSubtype, $recordId); [$normalizedType, $typeVariant] = $this->normalizeType($type, $message); $entry = [ 'entry_id' => 'odata-' . substr(hash('sha256', $ticketNo . '|' . ($row['Entry_No'] ?? '') . '|' . $timestampIso . '|' . $actor . '|' . $normalizedType . '|' . $message), 0, 20), 'ticket_no' => $ticketNo, 'timestamp_iso' => $timestampIso, 'date' => $date, 'time' => $this->normalizeTime($time), 'actor' => $actor, 'actor_role' => $this->normalizeActorRole($createdByType, $actor), 'type' => $normalizedType, 'type_variant' => $typeVariant, 'state_subtype' => $stateSubtype, 'message' => $message, 'source' => $soapMessage !== '' ? 'soap' : 'odata', ]; $entries[] = $entry; } return $entries; } /** * @return array> */ private function parseSoapEntries(string $ticketNo, string $communicationText, string $messageEntries): array { $entries = []; $candidates = []; if (!$this->isSoapPayloadEmpty($messageEntries)) { $candidates[] = $messageEntries; } if (!$this->isSoapPayloadEmpty($communicationText)) { $candidates[] = $communicationText; } foreach ($candidates as $candidate) { foreach ($this->extractEntriesFromPayload($ticketNo, $candidate) as $entry) { $entries[] = $entry; } } return $this->mergeEntries($entries, [], 200); } /** * @param array> $ticketLog */ private function buildSoapMessageEntriesFromTicketLog(array $ticketLog): string { $entryNos = []; foreach ($ticketLog as $row) { $type = trim((string) ($row['Type'] ?? '')); [$normalizedType, $typeVariant] = $this->normalizeType($type, ''); if ($typeVariant !== 'message' && !in_array($normalizedType, ['Message', 'Nachricht'], true)) { continue; } $entryNo = $this->normalizeEntryNo($row['Entry_No'] ?? null); if ($entryNo !== '') { $entryNos[$entryNo] = true; } } if ($entryNos === []) { return ''; } return implode(';', array_keys($entryNos)) . ';'; } /** * @return array */ private function extractSoapMessageMap(string $communicationText, string $messageEntries): array { $sources = [$communicationText, $messageEntries]; foreach ($sources as $payload) { $payload = trim($payload); if ($payload === '' || $this->isSoapPayloadEmpty($payload)) { continue; } $decoded = json_decode($payload, true); if (!is_array($decoded) || !array_is_list($decoded)) { continue; } $map = []; foreach ($decoded as $row) { if (!is_array($row)) { continue; } $entryNo = $this->normalizeEntryNo($row['EntryNo'] ?? $row['entryNo'] ?? $row['entry_no'] ?? null); if ($entryNo === '') { continue; } $message = $this->normalizeSoapMessageText((string) ($row['DataBlob'] ?? $row['dataBlob'] ?? $row['message'] ?? $row['text'] ?? '')); if ($message === '') { continue; } $map[$entryNo] = $message; } if ($map !== []) { return $map; } } return []; } /** * @return array> */ private function extractEntriesFromPayload(string $ticketNo, string $payload): array { $payload = trim($payload); if ($payload === '' || $this->isSoapPayloadEmpty($payload)) { return []; } $jsonDecoded = json_decode($payload, true); if (is_array($jsonDecoded) && array_is_list($jsonDecoded)) { $entries = []; foreach ($jsonDecoded as $row) { if (!is_array($row)) { continue; } $message = $this->normalizeSoapMessageText((string) ($row['message'] ?? $row['text'] ?? $row['DataBlob'] ?? $row['dataBlob'] ?? '')); if ($message === '') { continue; } $timestampIso = trim((string) ($row['timestamp'] ?? $row['created_at'] ?? '')); $actor = trim((string) ($row['actor'] ?? $row['user'] ?? '')); $createdByType = trim((string) ($row['created_by_type'] ?? $row['Created_By_Type'] ?? '')); $stateSubtype = trim((string) ($row['state_subtype'] ?? '')); [$type, $variant] = $this->normalizeType((string) ($row['type'] ?? 'Nachricht'), $message); [$date, $time] = $this->splitTimestamp($timestampIso); $entry = $this->buildSoapEntry( $ticketNo, $timestampIso, $date, $time, $actor, $this->normalizeActorRole($createdByType, $actor), $type, $variant, $stateSubtype, $message ); $entryNo = $this->normalizeEntryNo($row['EntryNo'] ?? $row['entryNo'] ?? $row['entry_no'] ?? null); if ($entryNo !== '') { $entry['entry_id'] = 'soap-' . substr(hash('sha256', $ticketNo . '|' . $entryNo . '|' . $timestampIso . '|' . $actor . '|' . $type . '|' . $message), 0, 20); } $entries[] = $entry; } if ($entries !== []) { return $entries; } } $normalized = str_replace(["\r\n", "\r"], "\n", $payload); $normalized = preg_replace('##i', "\n", $normalized) ?? $normalized; $normalized = strip_tags($normalized); $normalized = html_entity_decode($normalized, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $lines = preg_split('/\n+/', $normalized) ?: []; $lines = array_values(array_filter(array_map(static fn (string $line): string => trim($line), $lines), static fn (string $line): bool => $line !== '')); if ($lines === []) { return []; } $entries = []; foreach ($lines as $line) { $entries[] = $this->parseSoapLine($ticketNo, $line); } return $entries; } private function isSoapPayloadEmpty(string $payload): bool { $normalized = trim(html_entity_decode($payload, ENT_QUOTES | ENT_HTML5, 'UTF-8')); if ($normalized === '') { return true; } return in_array($normalized, ['0', '[]', '0[]', 'null', '{}'], true); } /** * @return array */ private function parseSoapLine(string $ticketNo, string $line): array { $working = trim($line); $timestampIso = ''; $date = ''; $time = ''; if (preg_match('/^(\d{4}-\d{2}-\d{2})(?:[T\s](\d{2}:\d{2}(?::\d{2})?))?\s*[-|]\s*(.+)$/u', $working, $m) === 1) { $date = $m[1]; $time = $this->normalizeTime($m[2]); $timestampIso = $this->combineDateTimeToIso($date, $time); $working = trim($m[3]); } elseif (preg_match('/^(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}(?::\d{2})?)\s*[-|]\s*(.+)$/u', $working, $m) === 1) { $date = $m[1]; $time = $this->normalizeTime($m[2]); $timestampIso = $this->convertEuropeanDateTimeToIso($date, $time); $working = trim($m[3]); } $actor = ''; $message = $working; if (preg_match('/^([^:]{2,80}):\s*(.+)$/u', $working, $m) === 1) { $actor = trim($m[1]); $message = trim($m[2]); } [$type, $variant] = $this->normalizeType('Nachricht', $message); $stateSubtype = ''; return $this->buildSoapEntry( $ticketNo, $timestampIso, $date, $time, $actor, $this->normalizeActorRole('', $actor), $type, $variant, $stateSubtype, $message ); } /** * @return array */ private function buildSoapEntry( string $ticketNo, string $timestampIso, string $date, string $time, string $actor, string $actorRole, string $type, string $variant, string $stateSubtype, string $message ): array { return [ 'entry_id' => 'soap-' . substr(hash('sha256', $ticketNo . '|' . $timestampIso . '|' . $actor . '|' . $type . '|' . $message), 0, 20), 'ticket_no' => $ticketNo, 'timestamp_iso' => $timestampIso, 'date' => $date, 'time' => $time, 'actor' => $actor, 'actor_role' => $actorRole, 'type' => $type, 'type_variant' => $variant, 'state_subtype' => $stateSubtype, 'message' => $message, 'source' => 'soap', ]; } /** * @param array> $primary * @param array> $secondary * @return array> */ private function mergeEntries(array $primary, array $secondary, int $maxEntries): array { $combined = [...$primary, ...$secondary]; if ($combined === []) { return []; } usort($combined, function (array $a, array $b): int { $ta = trim((string) ($a['timestamp_iso'] ?? '')); $tb = trim((string) ($b['timestamp_iso'] ?? '')); if ($ta === $tb) { return strcmp((string) ($b['entry_id'] ?? ''), (string) ($a['entry_id'] ?? '')); } if ($ta === '') { return 1; } if ($tb === '') { return -1; } return strcmp($tb, $ta); }); $deduped = []; $seen = []; foreach ($combined as $entry) { $signature = $this->entrySignature($entry); if (isset($seen[$signature])) { continue; } $seen[$signature] = true; $deduped[] = $entry; if (count($deduped) >= $maxEntries) { break; } } return $deduped; } /** * @param array $entry */ private function entrySignature(array $entry): string { return hash( 'sha256', implode('|', [ (string) ($entry['ticket_no'] ?? ''), (string) ($entry['timestamp_iso'] ?? ''), (string) ($entry['type'] ?? ''), hash('sha256', (string) ($entry['message'] ?? '')), ]) ); } /** * @param array> $entries */ private function hasFallbackOnlyEntries(array $entries): bool { if ($entries === []) { return false; } foreach ($entries as $entry) { if (($entry['source'] ?? '') === 'soap') { return false; } } return true; } private function buildODataMessage(string $type, string $stateSubtype, string $recordId): string { $type = trim($type); $recordId = trim($recordId); $stateSubtype = trim($stateSubtype); if ($type === 'Status' && $stateSubtype !== '') { return $stateSubtype; } if ($recordId !== '') { return $recordId; } if ($type !== '') { return $type; } return ''; } private function normalizeEntryNo(mixed $value): string { if (is_int($value)) { return (string) $value; } $raw = trim((string) $value); if ($raw === '' || preg_match('/^\d+$/', $raw) !== 1) { return ''; } return (string) ((int) $raw); } private function normalizeSoapMessageText(string $message): string { $message = trim($message); if ($message === '') { return ''; } $message = preg_replace('##i', "\n", $message) ?? $message; $message = preg_replace('#

\s*

#i', "\n\n", $message) ?? $message; $message = strip_tags($message); $message = html_entity_decode($message, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $message = str_replace(["\r\n", "\r"], "\n", $message); $message = preg_replace("/\n{3,}/", "\n\n", $message) ?? $message; return trim($message); } /** * @return array{0: string, 1: string} */ private function normalizeType(string $type, string $message): array { $type = trim($type); $needle = mb_strtolower($type . ' ' . $message); if ($type === 'Status' || str_contains($needle, 'status')) { return ['Status', 'status']; } if ($type === 'Weiterleitung' || str_contains($needle, 'weiterleit') || str_contains($needle, 'forward')) { return ['Weiterleitung', 'forward']; } if ($type === 'Datei' || str_contains($needle, 'datei') || str_contains($needle, 'file') || str_contains($needle, 'anhang')) { return ['Datei', 'file']; } if ($type === 'Nachricht' || $type === 'Message') { return ['Nachricht', 'message']; } if ($type !== '') { return [$type, 'other']; } return ['Nachricht', 'message']; } private function ticketSortStamp(array $ticket): int { $activity = trim((string) ($ticket['Last_Activity_Date'] ?? '')); if ($activity !== '' && $activity !== '0001-01-01T00:00:00Z') { $ts = strtotime($activity); if (is_int($ts)) { return $ts; } } $created = trim((string) ($ticket['Created_On'] ?? '')); if ($created !== '' && $created !== '0001-01-01T00:00:00Z') { $ts = strtotime($created); if (is_int($ts)) { return $ts; } } return 0; } private function combineDateTimeToIso(string $date, string $time): string { $date = trim($date); $time = $this->normalizeTime($time); if ($date === '') { return ''; } if ($time === '') { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1) { return $date . 'T00:00:00'; } return ''; } $ts = strtotime($date . ' ' . $time); if (!is_int($ts)) { return ''; } return date('Y-m-d\TH:i:s', $ts); } private function convertEuropeanDateTimeToIso(string $date, string $time): string { if (preg_match('/^(\d{2})\.(\d{2})\.(\d{4})$/', trim($date), $m) !== 1) { return ''; } return $this->combineDateTimeToIso($m[3] . '-' . $m[2] . '-' . $m[1], $time); } /** * @return array{0: string, 1: string} */ private function splitTimestamp(string $timestamp): array { $timestamp = trim($timestamp); if ($timestamp === '') { return ['', '']; } $ts = strtotime($timestamp); if (!is_int($ts)) { return ['', '']; } return [date('Y-m-d', $ts), date('H:i:s', $ts)]; } private function normalizeTime(string $time): string { $time = trim($time); if ($time === '') { return ''; } if (preg_match('/^(\d{2}:\d{2})(:\d{2})?/', $time, $m) === 1) { return isset($m[2]) && $m[2] !== '' ? ($m[1] . $m[2]) : ($m[1] . ':00'); } return $time; } /** * @param array|null $ticket Pre-fetched ticket data * @param array> $odataEntries * @param array> $soapEntries * @return array */ private function resolveContactActorNames(?array $ticket, array $odataEntries, array $soapEntries): array { $contactActorIds = $this->extractContactActorIds([...$odataEntries, ...$soapEntries]); if ($contactActorIds === []) { return []; } $currentContactName = trim((string) (($ticket['Current_Contact_Name'] ?? ''))); $customerName = trim((string) (($ticket['Company_Contact_Name'] ?? ''))); $nameMap = []; if ($customerName !== '') { try { $contacts = $this->bcODataGateway->getContactsForCustomer('', $customerName); foreach ($contacts as $contact) { $contactNo = strtoupper(trim((string) ($contact['No'] ?? ''))); $contactName = trim((string) ($contact['Name'] ?? '')); if ($contactNo !== '' && $contactName !== '') { $nameMap[$contactNo] = $contactName; } } } catch (\Throwable) { $nameMap = []; } } $resolved = []; foreach ($contactActorIds as $contactActorId) { if (isset($nameMap[$contactActorId])) { $resolved[$contactActorId] = $nameMap[$contactActorId]; } } if ($currentContactName !== '') { $remaining = array_values(array_filter( $contactActorIds, static fn (string $contactActorId): bool => !isset($resolved[$contactActorId]) )); if (count($remaining) === 1) { $resolved[$remaining[0]] = $currentContactName; } } return $resolved; } /** * @param array> $entries * @return array> */ private function applyActorNameMap(array $entries, array $actorNameMap): array { if ($entries === [] || $actorNameMap === []) { return $entries; } foreach ($entries as $index => $entry) { $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); if ($actor !== '' && isset($actorNameMap[$actor])) { $entries[$index]['actor'] = $actorNameMap[$actor]; } } return $entries; } /** * @param array> $entries * @return array */ private function extractContactActorIds(array $entries): array { $ids = []; foreach ($entries as $entry) { $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); if ($this->isContactActorId($actor)) { $ids[$actor] = true; } } return array_keys($ids); } private function isContactActorId(string $value): bool { return $value !== '' && preg_match('/^KT\d+$/', $value) === 1; } /** * @return array */ private function buildContactActorNameMapForDebitor(string $customerName): array { $customerName = trim($customerName); if ($customerName === '') { return []; } try { $contacts = $this->bcODataGateway->getContactsForCustomer('', $customerName); } catch (\Throwable) { return []; } $nameMap = []; foreach ($contacts as $contact) { if (!is_array($contact)) { continue; } $contactNo = strtoupper(trim((string) ($contact['No'] ?? ''))); $contactName = trim((string) ($contact['Name'] ?? '')); if ($contactNo === '' || $contactName === '') { continue; } $nameMap[$contactNo] = $contactName; } return $nameMap; } /** * Resolve support user actor codes to full names for a single ticket. * * Strategy: use the pre-fetched ticket's Support_User_Name, then look at * which support-type Created_By codes appear in the log entries. If there * is exactly one unique support code, map it to the Support_User_Name. * * @param array|null $ticket Pre-fetched ticket data * @param array> $odataEntries * @param array> $soapEntries * @return array */ private function resolveSupportActorNames(?array $ticket, array $odataEntries, array $soapEntries): array { $supportUserName = trim((string) ($ticket['Support_User_Name'] ?? '')); if ($supportUserName === '') { return []; } $result = []; foreach ([...$odataEntries, ...$soapEntries] as $entry) { $actorRole = trim((string) ($entry['actor_role'] ?? '')); if ($actorRole !== 'support') { continue; } $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); if ($actor !== '' && !$this->isContactActorId($actor) && mb_strlen($actor) <= 10) { $result[$actor] = $supportUserName; } } return $result; } /** * Build a support user code→name map from already-fetched communication entries. * * Groups entries by ticket_no, finds unique support-type actor codes per * ticket, and maps each code to the ticket's Support_User_Name (from the * pre-built $supportUserNamesByTicket lookup). No additional API calls. * * @param array> $entries * @param array $supportUserNamesByTicket ticket_no → Support_User_Name * @return array code → full name */ private function buildSupportActorNameMapFromEntries(array $entries, array $supportUserNamesByTicket): array { if ($entries === [] || $supportUserNamesByTicket === []) { return []; } // Map support actor codes to names using ticket assignment data. // For each entry with a support role, look up the ticket's Support_User_Name // and map the actor code to that name. This handles tickets with multiple // support codes (e.g. after reassignment) robustly. $result = []; foreach ($entries as $entry) { $actorRole = trim((string) ($entry['actor_role'] ?? '')); if ($actorRole !== 'support') { continue; } $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); if ($actor === '' || $this->isContactActorId($actor) || mb_strlen($actor) > 10) { continue; } $ticketNo = trim((string) ($entry['ticket_no'] ?? '')); if ($ticketNo === '') { continue; } $supportUserName = $supportUserNamesByTicket[$ticketNo] ?? ''; if ($supportUserName !== '' && !isset($result[$actor])) { $result[$actor] = $supportUserName; } } return $result; } private function normalizeActorRole(string $createdByType, string $actor): string { $type = mb_strtolower(trim($createdByType)); if ($type !== '') { if ( str_contains($type, 'kontakt') || str_contains($type, 'contact') || str_contains($type, 'customer') || str_contains($type, 'debitor') || str_contains($type, 'client') || str_contains($type, 'external') ) { return 'customer'; } if ( str_contains($type, 'user') || str_contains($type, 'benutzer') || str_contains($type, 'support') || str_contains($type, 'agent') || str_contains($type, 'internal') ) { return 'support'; } } $actorCode = strtoupper(trim($actor)); if ($this->isContactActorId($actorCode)) { return 'customer'; } return 'support'; } }