netgescon-day0/app/Http/Controllers/Api/PanasonicCstaController.php

825 lines
32 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\ChiamataPostIt;
use App\Models\CommunicationMessage;
use App\Models\PbxClickToCallRequest;
use App\Models\RubricaUniversale;
use App\Services\Cti\PbxRoutingService;
use App\Support\PhoneNumber;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class PanasonicCstaController extends Controller
{
public function incoming(Request $request): JsonResponse
{
if (! $this->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;
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' => $payload['called_at'] ?? 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,
]);
$normalized = PhoneNumber::normalizeForMatch((string) ($payload['phone'] ?? ''));
$direction = $this->normalizeDirection((string) ($payload['direction'] ?? ''));
$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' => $payload['ended_at'] ?? 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 = $payload['ended_at'] ?? 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,
]);
}
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' => $payload['called_at'] ?? 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'] = ($payload['ended_at'] ?? 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'] = ($payload['ended_at'] ?? 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<string,mixed>|null $existing
* @param array<string,mixed> $newData
* @return array<string,mixed>
*/
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;
}
/**
* @return array<string,mixed>
*/
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(),
];
}
}