isAuthorized($request)) { return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $payload = $request->validate([ 'phone' => ['required', 'string', 'max:64'], 'name' => ['nullable', 'string', 'max:255'], 'direction' => ['nullable', 'in:in_arrivo,in_uscita,persa'], 'outcome' => ['nullable', 'string', 'max:255'], 'event_id' => ['nullable', 'string', 'max:100'], 'called_extension' => ['nullable', 'string', 'max:30'], 'note' => ['nullable', 'string'], 'called_at' => ['nullable', 'date'], ]); $normalized = PhoneNumber::normalizeForMatch((string) $payload['phone']); $rubrica = $this->findRubricaByPhone($normalized); $postIt = null; if (Schema::hasTable('chiamate_post_it')) { $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 ?: ($payload['name'] ?? null), 'direzione' => $payload['direction'] ?? 'in_arrivo', 'esito' => $payload['outcome'] ?? null, 'origine' => 'panasonic_ns1000', 'origine_id' => $payload['event_id'] ?? null, 'oggetto' => 'Chiamata ' . (($payload['direction'] ?? 'in_arrivo') === 'in_arrivo' ? 'in arrivo' : 'telefonica'), 'nota' => $this->buildNote($payload), 'priorita' => 'Media', 'stato' => 'post_it', 'chiamata_il' => $payload['called_at'] ?? now(), ]); } return response()->json([ 'ok' => true, 'source' => 'panasonic_ns1000', '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, 'called_extension' => $payload['called_extension'] ?? null, ]); } public function lookup(Request $request): JsonResponse { if (! $this->isAuthorized($request)) { 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)) { return response()->json(['ok' => false, 'message' => 'Unauthorized'], 401); } $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', 'in:in_arrivo,in_uscita,persa'], 'called_extension' => ['nullable', 'string', 'max:30'], 'note' => ['nullable', 'string'], ]); $normalized = PhoneNumber::normalizeForMatch((string) ($payload['phone'] ?? '')); $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' => $payload['direction'] ?? 'in_arrivo', '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) { return response()->json([ 'ok' => false, 'message' => 'No matching Post-it and table unavailable', ], 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(); return response()->json([ 'ok' => true, 'source' => 'panasonic_ns1000', 'closed' => true, 'created_new' => $created, 'post_it_id' => $postIt->id, 'stato' => $postIt->stato, 'esito' => $postIt->esito, 'durata_secondi' => Schema::hasColumn('chiamate_post_it', 'durata_secondi') ? $postIt->durata_secondi : null, ]); } private function isAuthorized(Request $request): bool { $configured = (string) env('NETGESCON_CTI_SHARED_TOKEN', ''); if ($configured === '') { return false; } $token = (string) ($request->header('X-CTI-Token') ?: $request->input('token', '')); return hash_equals($configured, $token); } 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 { $lines = [ 'Origine: Panasonic NS1000 (CSTA)', ]; if (! empty($payload['called_extension'])) { $lines[] = 'Interno chiamato: ' . $payload['called_extension']; } if (! empty($payload['event_id'])) { $lines[] = 'Event ID: ' . $payload['event_id']; } if (! empty($payload['note'])) { $lines[] = 'Note PBX: ' . $payload['note']; } return implode("\n", $lines); } }