isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'incoming'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $traceId = (string) Str::uuid(); $payload = $request->validate([ 'phone' => ['required', 'string', 'max:64'], 'name' => ['nullable', 'string', 'max:255'], 'direction' => ['nullable', 'string', 'max:32'], 'outcome' => ['nullable', 'string', 'max:255'], 'event_id' => ['nullable', 'string', 'max:100'], 'called_extension' => ['nullable', 'string', 'max:30'], 'calling_extension' => ['nullable', 'string', 'max:30'], 'event_type' => ['nullable', 'string', 'max:60'], 'note' => ['nullable', 'string'], 'called_at' => ['nullable', 'date'], 'is_test' => ['nullable', 'boolean'], ]); [$isTest, $classification] = $this->classifyCall($payload); $this->logTrace('incoming.received', $request, $payload, [ 'trace_id' => $traceId, 'classification' => $classification, 'is_test' => $isTest, ]); $normalized = PhoneNumber::normalizeForMatch((string) $payload['phone']); $rubrica = $this->findRubricaByPhone($normalized); $direction = $this->normalizeDirection((string) ($payload['direction'] ?? '')); $routing = app(PbxRoutingService::class)->resolveByExtension((string) ($payload['called_extension'] ?? '')); $postIt = null; $calledAt = $this->normalizePayloadDateTime($payload['called_at'] ?? null); if (Schema::hasTable('chiamate_post_it')) { $postIt = ChiamataPostIt::query()->create([ 'stabile_id' => $routing['stabile_id'], 'rubrica_id' => $rubrica?->id, 'ticket_id' => null, 'creato_da_user_id' => $routing['user_id'], 'telefono' => $normalized !== '' ? $normalized : (string) $payload['phone'], 'nome_chiamante' => $rubrica?->nome_completo ?: ($payload['name'] ?? null), 'direzione' => $direction['post_it'], 'esito' => $payload['outcome'] ?? null, 'origine' => 'panasonic_ns1000', 'origine_id' => $payload['event_id'] ?? null, 'oggetto' => 'Chiamata ' . ($direction['post_it'] === 'in_uscita' ? 'telefonica' : 'in arrivo'), 'nota' => $this->buildNote($payload), 'priorita' => 'Media', 'stato' => 'post_it', 'chiamata_il' => $calledAt ?? now(), ]); } $message = $this->storeIncomingCommunicationMessage( $payload, $normalized, $routing, $rubrica, $direction['communication'], $postIt?->id ); $this->mirrorToPeerIfEnabled($request, 'incoming', $payload); $this->logTrace('incoming.stored', $request, $payload, [ 'trace_id' => $traceId, 'post_it_id' => $postIt?->id, 'message_id' => $message?->id, 'rubrica_id' => $rubrica?->id, 'classification' => $classification, 'is_test' => $isTest, ]); return response()->json([ 'ok' => true, 'source' => 'panasonic_ns1000', 'trace_id' => $traceId, 'phone_normalized' => $normalized, 'matched' => (bool) $rubrica, 'rubrica' => $rubrica ? [ 'id' => $rubrica->id, 'nome_completo' => $rubrica->nome_completo, 'categoria' => $rubrica->categoria, ] : null, 'post_it_id' => $postIt?->id, 'communication_message_id' => $message?->id, 'assigned_user_id' => $routing['user_id'], 'amministratore_id' => $routing['amministratore_id'], 'studio_role' => $routing['studio_role'], 'studio_mode' => $routing['studio_mode'], 'called_extension' => $payload['called_extension'] ?? null, ]); } public function lookup(Request $request): JsonResponse { if (! $this->isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'lookup'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $data = $request->validate([ 'phone' => ['required', 'string', 'max:64'], ]); $normalized = PhoneNumber::normalizeForMatch((string) $data['phone']); $rubrica = $this->findRubricaByPhone($normalized); return response()->json([ 'ok' => true, 'phone_normalized' => $normalized, 'matched' => (bool) $rubrica, 'rubrica' => $rubrica ? [ 'id' => $rubrica->id, 'nome_completo' => $rubrica->nome_completo, 'telefono_ufficio' => $rubrica->telefono_ufficio, 'telefono_cellulare' => $rubrica->telefono_cellulare, 'telefono_casa' => $rubrica->telefono_casa, 'categoria' => $rubrica->categoria, ] : null, ]); } public function callEnded(Request $request): JsonResponse { if (! $this->isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'call-ended'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $traceId = (string) Str::uuid(); $payload = $request->validate([ 'phone' => ['nullable', 'string', 'max:64', 'required_without:event_id'], 'event_id' => ['nullable', 'string', 'max:100', 'required_without:phone'], 'outcome' => ['nullable', 'string', 'max:255'], 'duration_seconds' => ['nullable', 'integer', 'min:0', 'max:86400'], 'ended_at' => ['nullable', 'date'], 'direction' => ['nullable', 'string', 'max:32'], 'called_extension' => ['nullable', 'string', 'max:30'], 'calling_extension' => ['nullable', 'string', 'max:30'], 'event_type' => ['nullable', 'string', 'max:60'], 'note' => ['nullable', 'string'], 'is_test' => ['nullable', 'boolean'], ]); [$isTest, $classification] = $this->classifyCall($payload); $this->logTrace('call-ended.received', $request, $payload, [ 'trace_id' => $traceId, 'classification' => $classification, 'is_test' => $isTest, ]); try { $normalized = PhoneNumber::normalizeForMatch((string) ($payload['phone'] ?? '')); $direction = $this->normalizeDirection((string) ($payload['direction'] ?? '')); $endedAt = $this->normalizePayloadDateTime($payload['ended_at'] ?? null); $postIt = $this->findOpenPostIt( (string) ($payload['event_id'] ?? ''), $normalized !== '' ? $normalized : (string) ($payload['phone'] ?? '') ); $created = false; if (! $postIt && Schema::hasTable('chiamate_post_it')) { $created = true; $rubrica = $this->findRubricaByPhone($normalized); $postIt = ChiamataPostIt::query()->create([ 'stabile_id' => null, 'rubrica_id' => $rubrica?->id, 'ticket_id' => null, 'creato_da_user_id' => null, 'telefono' => $normalized !== '' ? $normalized : (string) ($payload['phone'] ?? ''), 'nome_chiamante' => $rubrica?->nome_completo, 'direzione' => $direction['post_it'], 'esito' => $payload['outcome'] ?? null, 'origine' => 'panasonic_ns1000', 'origine_id' => $payload['event_id'] ?? null, 'oggetto' => 'Chiamata terminata (evento senza incoming)', 'nota' => $this->buildNote($payload), 'priorita' => 'Media', 'stato' => 'post_it', 'chiamata_il' => $endedAt ?? now(), ]); } if (! $postIt) { $this->logTrace('call-ended.not-found', $request, $payload, [ 'trace_id' => $traceId, 'classification' => $classification, 'is_test' => $isTest, ]); return response()->json([ 'ok' => false, 'message' => 'No matching Post-it and table unavailable', 'trace_id' => $traceId, ], 404); } $postIt->esito = $payload['outcome'] ?? $postIt->esito; $postIt->stato = 'chiusa'; if (Schema::hasColumn('chiamate_post_it', 'durata_secondi')) { $postIt->durata_secondi = $payload['duration_seconds'] ?? $postIt->durata_secondi; } if (Schema::hasColumn('chiamate_post_it', 'chiusa_il')) { $postIt->chiusa_il = $endedAt ?? now(); } if (! empty($payload['note'])) { $existing = trim((string) ($postIt->nota ?? '')); $suffix = 'Note chiusura PBX: ' . $payload['note']; $postIt->nota = $existing !== '' ? ($existing . "\n" . $suffix) : $suffix; } $postIt->save(); $message = $this->closeCommunicationMessage($payload, $normalized, $direction['communication'], $postIt->id); $this->mirrorToPeerIfEnabled($request, 'call-ended', $payload); $this->logTrace('call-ended.closed', $request, $payload, [ 'trace_id' => $traceId, 'post_it_id' => $postIt->id, 'message_id' => $message?->id, 'created_new' => $created, 'classification' => $classification, 'is_test' => $isTest, ]); return response()->json([ 'ok' => true, 'source' => 'panasonic_ns1000', 'trace_id' => $traceId, 'closed' => true, 'created_new' => $created, 'post_it_id' => $postIt->id, 'communication_message_id' => $message?->id, 'stato' => $postIt->stato, 'esito' => $postIt->esito, 'durata_secondi' => Schema::hasColumn('chiamate_post_it', 'durata_secondi') ? $postIt->durata_secondi : null, ]); } catch (\Throwable $e) { Log::error('CTI Panasonic call-ended failed', [ 'trace_id' => $traceId, 'event_id' => (string) ($payload['event_id'] ?? ''), 'called_extension' => (string) ($payload['called_extension'] ?? ''), 'calling_extension' => (string) ($payload['calling_extension'] ?? ''), 'direction' => (string) ($payload['direction'] ?? ''), 'phone' => (string) ($payload['phone'] ?? ''), 'error' => $e->getMessage(), 'exception' => get_class($e), ]); return response()->json([ 'ok' => false, 'message' => 'Call ended processing failed', 'trace_id' => $traceId, 'error' => $e->getMessage(), ], 500); } } public function pendingClickToCall(Request $request): JsonResponse { if (! $this->isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'click-to-call/pending'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $data = $request->validate([ 'extension' => ['nullable', 'string', 'max:30'], 'limit' => ['nullable', 'integer', 'min:1', 'max:100'], ]); $extension = app(PbxRoutingService::class)->normalizeExtension((string) ($data['extension'] ?? '')); $limit = (int) ($data['limit'] ?? 20); $requests = PbxClickToCallRequest::query() ->where('status', 'pending') ->when($extension !== '', function ($query) use ($extension): void { $query->where('source_extension', $extension); }) ->orderByRaw('COALESCE(requested_at, created_at) asc') ->limit($limit) ->get(); return response()->json([ 'ok' => true, 'count' => $requests->count(), 'requests' => $requests->map(fn(PbxClickToCallRequest $clickToCallRequest): array=> $this->serializeClickToCallRequest($clickToCallRequest))->values(), ]); } public function claimClickToCall(Request $request, PbxClickToCallRequest $clickToCallRequest): JsonResponse { if (! $this->isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'click-to-call/claim'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $data = $request->validate([ 'adapter' => ['nullable', 'string', 'max:120'], 'note' => ['nullable', 'string'], ]); $clickToCallRequest->refresh(); if ($clickToCallRequest->status !== 'pending') { return response()->json([ 'ok' => false, 'message' => 'Request not pending anymore', 'status' => $clickToCallRequest->status, ], 409); } $clickToCallRequest->status = 'processing'; $clickToCallRequest->metadata = $this->mergeMetadata($clickToCallRequest->metadata, [ 'adapter' => (string) ($data['adapter'] ?? ''), 'claimed_at' => now()->toIso8601String(), 'claim_note' => $data['note'] ?? null, ]); $clickToCallRequest->save(); return response()->json([ 'ok' => true, 'request' => $this->serializeClickToCallRequest($clickToCallRequest), ]); } public function updateClickToCallStatus(Request $request, PbxClickToCallRequest $clickToCallRequest): JsonResponse { if (! $this->isAuthorized($request)) { $this->logUnauthorizedAttempt($request, 'click-to-call/status'); return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $data = $request->validate([ 'status' => ['required', 'in:processing,accepted,completed,failed,cancelled'], 'note' => ['nullable', 'string'], 'event_id' => ['nullable', 'string', 'max:100'], 'provider_call_id' => ['nullable', 'string', 'max:120'], 'called_extension' => ['nullable', 'string', 'max:30'], 'duration_seconds' => ['nullable', 'integer', 'min:0', 'max:86400'], 'outcome' => ['nullable', 'string', 'max:255'], 'error_code' => ['nullable', 'string', 'max:80'], 'error_message' => ['nullable', 'string', 'max:255'], ]); $clickToCallRequest->status = (string) $data['status']; if (in_array($clickToCallRequest->status, ['completed', 'failed', 'cancelled'], true)) { $clickToCallRequest->processed_at = now(); } if (! empty($data['note'])) { $clickToCallRequest->note = trim(implode("\n", array_filter([ (string) ($clickToCallRequest->note ?? ''), (string) $data['note'], ]))); } $clickToCallRequest->metadata = $this->mergeMetadata($clickToCallRequest->metadata, [ 'event_id' => $data['event_id'] ?? null, 'provider_call_id' => $data['provider_call_id'] ?? null, 'called_extension' => app(PbxRoutingService::class)->normalizeExtension((string) ($data['called_extension'] ?? '')), 'duration_seconds' => $data['duration_seconds'] ?? null, 'outcome' => $data['outcome'] ?? null, 'error_code' => $data['error_code'] ?? null, 'error_message' => $data['error_message'] ?? null, 'status_updated_at' => now()->toIso8601String(), ]); $clickToCallRequest->save(); return response()->json([ 'ok' => true, 'request' => $this->serializeClickToCallRequest($clickToCallRequest), ]); } private function isAuthorized(Request $request): bool { $configured = $this->resolveConfiguredToken(); if ($configured === '') { return false; } $token = (string) ($request->header('X-CTI-Token') ?: $request->input('token', '')); return hash_equals($configured, $token); } private function resolveConfiguredToken(): string { $configured = trim((string) config('cti.shared_token', '')); if ($configured !== '') { return $configured; } foreach ((array) config('cti.legacy_tokens', []) as $legacyToken) { $candidate = trim((string) $legacyToken); if ($candidate !== '') { return $candidate; } } $configured = (string) env('NETGESCON_CTI_SHARED_TOKEN', ''); if ($configured === '') { $configured = (string) env('CTI_SHARED_SECRET', ''); } if ($configured === '') { $configured = (string) env('CTI_SHARED_TOKEN', ''); } if ($configured === '') { $configured = (string) env('CTI_TOKEN', ''); } if ($configured === '') { $configured = (string) env('CTI_PANASONIC_TOKEN', ''); } return $configured; } private function mirrorToPeerIfEnabled(Request $request, string $endpoint, array $payload): void { $enabled = (bool) config('cti.mirror_enabled', false); if (! $enabled) { return; } if ((string) $request->header('X-CTI-Mirrored', '0') === '1') { return; } $baseUrl = rtrim((string) config('cti.mirror_base_url', ''), '/'); if ($baseUrl === '') { return; } $token = $this->resolveConfiguredToken(); if ($token === '') { return; } $timeout = (int) config('cti.mirror_timeout_seconds', 2); $url = $baseUrl . '/api/v1/cti/panasonic/' . ltrim($endpoint, '/'); try { Http::timeout(max(1, $timeout)) ->withHeaders([ 'X-CTI-Token' => $token, 'X-CTI-Mirrored' => '1', ]) ->post($url, $payload); } catch (\Throwable $e) { Log::warning('CTI mirror failed', [ 'endpoint' => $endpoint, 'url' => $url, 'error' => $e->getMessage(), ]); } } private function findRubricaByPhone(string $phone): ?RubricaUniversale { $digits = PhoneNumber::normalizeDigits($phone); if ($digits === '') { return null; } $tail = substr($digits, -7); if ($tail === false || $tail === '') { return null; } return RubricaUniversale::query() ->where(function ($q) use ($tail) { $q->orWhere('telefono_cellulare', 'like', '%' . $tail . '%') ->orWhere('telefono_ufficio', 'like', '%' . $tail . '%') ->orWhere('telefono_casa', 'like', '%' . $tail . '%') ->orWhere('telefono_cellulare_nazionale', 'like', '%' . $tail . '%'); }) ->orderByDesc('updated_at') ->first(); } private function findOpenPostIt(string $eventId, string $phone): ?ChiamataPostIt { if (! Schema::hasTable('chiamate_post_it')) { return null; } if ($eventId !== '') { $byEvent = ChiamataPostIt::query() ->where('origine', 'panasonic_ns1000') ->where('origine_id', $eventId) ->where('stato', '!=', 'chiusa') ->orderByDesc('id') ->first(); if ($byEvent) { return $byEvent; } } $digits = PhoneNumber::normalizeDigits($phone); if ($digits === '') { return null; } $tail = substr($digits, -7); if ($tail === false || $tail === '') { return null; } return ChiamataPostIt::query() ->where('stato', '!=', 'chiusa') ->where('origine', 'panasonic_ns1000') ->where('telefono', 'like', '%' . $tail . '%') ->orderByDesc('chiamata_il') ->first(); } private function buildNote(array $payload): string { [$isTest, $classification] = $this->classifyCall($payload); $direction = $this->normalizeDirection((string) ($payload['direction'] ?? '')); $lines = [ 'Origine: Panasonic NS1000 (CSTA)', 'Classificazione: ' . $classification, 'Direzione normalizzata: ' . $direction['communication'], ]; if ($isTest) { $lines[] = 'Flag test: SI'; } if (! empty($payload['called_extension'])) { $lines[] = 'Interno chiamato: ' . $payload['called_extension']; } if (! empty($payload['calling_extension'])) { $lines[] = 'Interno chiamante: ' . $payload['calling_extension']; } if (! empty($payload['event_type'])) { $lines[] = 'Evento CSTA: ' . $payload['event_type']; } if (! empty($payload['event_id'])) { $lines[] = 'Event ID: ' . $payload['event_id']; } if (! empty($payload['note'])) { $lines[] = 'Note PBX: ' . $payload['note']; } return implode("\n", $lines); } /** * @return array{0:bool,1:string} */ private function classifyCall(array $payload): array { if (array_key_exists('is_test', $payload)) { $isTest = (bool) $payload['is_test']; return [$isTest, $isTest ? 'test (flag esplicito)' : 'reale presunta (flag esplicito)']; } $haystack = mb_strtolower(trim(implode(' ', [ (string) ($payload['name'] ?? ''), (string) ($payload['note'] ?? ''), (string) ($payload['event_id'] ?? ''), ]))); foreach (['test', 'prova', 'demo', 'simulazione'] as $marker) { if ($haystack !== '' && str_contains($haystack, $marker)) { return [true, 'test probabile (marker: ' . $marker . ')']; } } return [false, 'reale presunta']; } private function logUnauthorizedAttempt(Request $request, string $endpoint): void { Log::warning('CTI Panasonic unauthorized', [ 'endpoint' => $endpoint, 'ip' => $request->ip(), 'user_agent' => (string) $request->userAgent(), 'has_token_header' => $request->headers->has('X-CTI-Token'), ]); } private function logTrace(string $event, Request $request, array $payload, array $extra = []): void { $phoneRaw = (string) ($payload['phone'] ?? ''); $phoneDigits = PhoneNumber::normalizeDigits($phoneRaw); Log::info('CTI Panasonic trace', array_merge([ 'event' => $event, 'ip' => $request->ip(), 'user_agent' => (string) $request->userAgent(), 'mirrored' => (string) $request->header('X-CTI-Mirrored', '0') === '1', 'event_type' => (string) ($payload['event_type'] ?? ''), 'event_id' => (string) ($payload['event_id'] ?? ''), 'calling_extension' => (string) ($payload['calling_extension'] ?? ''), 'called_extension' => (string) ($payload['called_extension'] ?? ''), 'direction' => (string) ($payload['direction'] ?? ''), 'phone_tail' => $phoneDigits !== '' ? substr($phoneDigits, -4) : '', ], $extra)); } private function storeIncomingCommunicationMessage( array $payload, string $normalizedPhone, array $routing, ?RubricaUniversale $rubrica, string $direction, ?int $postItId ): ?CommunicationMessage { if (! Schema::hasTable('communication_messages')) { return null; } $message = new CommunicationMessage([ 'channel' => 'panasonic_csta', 'direction' => $direction, 'external_chat_id' => (string) ($payload['called_extension'] ?? ''), 'external_message_id' => (string) ($payload['event_id'] ?? ''), 'phone_number' => $normalizedPhone !== '' ? $normalizedPhone : (string) $payload['phone'], 'sender_name' => $rubrica?->nome_completo ?: ($payload['name'] ?? null), 'message_text' => $this->buildNote($payload), 'attachments' => null, 'status' => 'received', 'received_at' => $this->normalizePayloadDateTime($payload['called_at'] ?? null) ?? now(), 'metadata' => [ 'provider' => 'panasonic_ns1000', 'amministratore_id' => $routing['amministratore_id'], 'studio_role' => $routing['studio_role'], 'studio_mode' => $routing['studio_mode'], 'called_extension' => $routing['extension'], 'calling_extension' => app(PbxRoutingService::class)->normalizeExtension((string) ($payload['calling_extension'] ?? '')), 'event_id' => (string) ($payload['event_id'] ?? ''), 'event_type' => (string) ($payload['event_type'] ?? 'Delivered'), 'rubrica_id' => $rubrica?->id, 'post_it_id' => $postItId, 'raw_direction' => (string) ($payload['direction'] ?? ''), 'outcome' => $payload['outcome'] ?? null, 'note' => $payload['note'] ?? null, ], ]); if (Schema::hasColumn('communication_messages', 'target_extension')) { $message->target_extension = $routing['extension'] !== '' ? $routing['extension'] : null; } if (Schema::hasColumn('communication_messages', 'assigned_user_id')) { $message->assigned_user_id = $routing['user_id']; } if (Schema::hasColumn('communication_messages', 'stabile_id')) { $message->stabile_id = $routing['stabile_id']; } if (Schema::hasColumn('communication_messages', 'post_it_id')) { $message->post_it_id = $postItId; } $message->save(); return $message; } private function closeCommunicationMessage(array $payload, string $normalizedPhone, string $direction, int $postItId): ?CommunicationMessage { if (! Schema::hasTable('communication_messages')) { return null; } $message = $this->findCommunicationMessage((string) ($payload['event_id'] ?? ''), $normalizedPhone !== '' ? $normalizedPhone : (string) ($payload['phone'] ?? '')); if (! $message) { $routing = app(PbxRoutingService::class)->resolveByExtension((string) ($payload['called_extension'] ?? '')); $message = $this->storeIncomingCommunicationMessage( $payload, $normalizedPhone, $routing, $this->findRubricaByPhone($normalizedPhone), $direction, $postItId ); if ($message) { $metadata = is_array($message->metadata) ? $message->metadata : []; $metadata['ended_at'] = ($this->normalizePayloadDateTime($payload['ended_at'] ?? null) ?? now())->toDateTimeString(); $metadata['duration_seconds'] = $payload['duration_seconds'] ?? null; $metadata['outcome'] = $payload['outcome'] ?? null; $metadata['event_type'] = (string) ($payload['event_type'] ?? 'ConnectionCleared'); $message->status = 'completed'; $message->metadata = $metadata; $message->save(); } return $message; } $metadata = is_array($message->metadata) ? $message->metadata : []; $metadata['ended_at'] = ($this->normalizePayloadDateTime($payload['ended_at'] ?? null) ?? now())->toDateTimeString(); $metadata['duration_seconds'] = $payload['duration_seconds'] ?? null; $metadata['outcome'] = $payload['outcome'] ?? null; $metadata['event_type'] = (string) ($payload['event_type'] ?? 'ConnectionCleared'); $metadata['note'] = $payload['note'] ?? ($metadata['note'] ?? null); $metadata['post_it_id'] = $postItId; $message->status = 'completed'; $message->message_text = $this->buildNote($payload); $message->metadata = $metadata; if (Schema::hasColumn('communication_messages', 'post_it_id')) { $message->post_it_id = $postItId; } $message->save(); return $message; } private function findCommunicationMessage(string $eventId, string $phone): ?CommunicationMessage { if ($eventId !== '') { $byEvent = CommunicationMessage::query() ->where('channel', 'panasonic_csta') ->where('external_message_id', $eventId) ->orderByDesc('id') ->first(); if ($byEvent) { return $byEvent; } } $digits = PhoneNumber::normalizeDigits($phone); if ($digits === '') { return null; } $tail = substr($digits, -7); if ($tail === false || $tail === '') { return null; } return CommunicationMessage::query() ->where('channel', 'panasonic_csta') ->where('phone_number', 'like', '%' . $tail . '%') ->orderByDesc('id') ->first(); } /** * @return array{post_it:string,communication:string} */ private function normalizeDirection(string $direction): array { $value = mb_strtolower(trim($direction)); return match ($value) { 'in_uscita', 'outbound', 'outgoing' => ['post_it' => 'in_uscita', 'communication' => 'outbound'], 'persa', 'missed', 'no_answer' => ['post_it' => 'persa', 'communication' => 'inbound'], default => ['post_it' => 'in_arrivo', 'communication' => 'inbound'], }; } /** * @param array|null $existing * @param array $newData * @return array */ private function mergeMetadata(?array $existing, array $newData): array { $base = is_array($existing) ? $existing : []; foreach ($newData as $key => $value) { if ($value === null || $value === '') { continue; } $base[$key] = $value; } return $base; } private function normalizePayloadDateTime(mixed $value): ?CarbonInterface { if ($value instanceof CarbonInterface) { return $value; } if ($value instanceof \DateTimeInterface) { return now()->setTimestamp($value->getTimestamp()); } $raw = trim((string) ($value ?? '')); if ($raw === '') { return null; } try { return now()->parse($raw); } catch (\Throwable) { return null; } } /** * @return array */ private function serializeClickToCallRequest(PbxClickToCallRequest $clickToCallRequest): array { return [ 'id' => $clickToCallRequest->id, 'requested_by_user_id' => $clickToCallRequest->requested_by_user_id, 'assigned_user_id' => $clickToCallRequest->assigned_user_id, 'stabile_id' => $clickToCallRequest->stabile_id, 'communication_message_id' => $clickToCallRequest->communication_message_id, 'source_extension' => $clickToCallRequest->source_extension, 'target_number' => $clickToCallRequest->target_number, 'status' => $clickToCallRequest->status, 'note' => $clickToCallRequest->note, 'requested_at' => optional($clickToCallRequest->requested_at)->toIso8601String(), 'processed_at' => optional($clickToCallRequest->processed_at)->toIso8601String(), 'metadata' => $clickToCallRequest->metadata, 'created_at' => optional($clickToCallRequest->created_at)->toIso8601String(), 'updated_at' => optional($clickToCallRequest->updated_at)->toIso8601String(), ]; } }