From 4f643c8bef4434d32bbaebe8ca2a74aaff08ab89 Mon Sep 17 00:00:00 2001 From: michele Date: Wed, 11 Mar 2026 17:50:04 +0000 Subject: [PATCH] feat(cti): add Panasonic NS1000 webhook+lookup and restart runbook --- .../Api/PanasonicCstaController.php | 154 ++++++++++++++++++ docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md | 88 ++++++++++ docs/ai/restart/START-HERE-NETGESCON-DAY0.md | 3 +- routes/api.php | 9 + scripts/ops/netgescon-panasonic-csta-test.sh | 62 +++++++ 5 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/PanasonicCstaController.php create mode 100644 docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md create mode 100644 scripts/ops/netgescon-panasonic-csta-test.sh diff --git a/app/Http/Controllers/Api/PanasonicCstaController.php b/app/Http/Controllers/Api/PanasonicCstaController.php new file mode 100644 index 0000000..bd9f725 --- /dev/null +++ b/app/Http/Controllers/Api/PanasonicCstaController.php @@ -0,0 +1,154 @@ +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, + ]); + } + + 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 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); + } +} diff --git a/docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md b/docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md new file mode 100644 index 0000000..1f8bf65 --- /dev/null +++ b/docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md @@ -0,0 +1,88 @@ +# CTI Panasonic NS1000 - Setup operativo + +Obiettivo: intercettare chiamate in arrivo dal centralino Panasonic NS1000 (CSTA porta 33333) e creare automaticamente Post-it in NetGescon con filtro rubrica. + +## Prerequisiti verificati + +- Connessione TCP PBX CSTA: `192.168.0.101:33333` raggiungibile. +- Modello centralino: Panasonic NS1000. + +## 1) Configurazione applicazione NetGescon + +Aggiungi in `.env` del nodo applicativo: + +```env +NETGESCON_CTI_SHARED_TOKEN= +``` + +Poi: + +```bash +cd /home/michele/netgescon/netgescon-day0 +php artisan optimize:clear +php artisan config:clear +php artisan route:clear +``` + +## 2) Endpoint disponibili + +- `POST /api/v1/cti/panasonic/incoming` + - crea Post-it chiamata in `chiamate_post_it` + - fa match automatico numero -> `rubrica_universale` +- `GET /api/v1/cti/panasonic/lookup?phone=...` + - solo lookup rubrica (match yes/no) + +Autenticazione: header `X-CTI-Token: `. + +## 3) Test rapido terminale + +```bash +cd /home/michele/netgescon/netgescon-day0 +bash scripts/ops/netgescon-panasonic-csta-test.sh \ + --pbx-ip 192.168.0.101 \ + --pbx-port 33333 \ + --app-url http://127.0.0.1:8000 \ + --token "" \ + --phone "+393331112233" \ + --ext "101" +``` + +## 4) Esempio comando da usare lato interfaccia centralino + +Se il centralino puo inviare HTTP su evento incoming call, usa: + +```bash +curl -sS -X POST "http://:8000/api/v1/cti/panasonic/incoming" \ + -H "Content-Type: application/json" \ + -H "X-CTI-Token: " \ + -d '{ + "phone": "", + "name": "", + "direction": "in_arrivo", + "called_extension": "", + "event_id": "", + "note": "evento CSTA NS1000" + }' +``` + +Per filtro rubrica (prima del popup interno): + +```bash +curl -sS "http://:8000/api/v1/cti/panasonic/lookup?phone=" \ + -H "X-CTI-Token: " +``` + +## 5) Cosa fa il match numeri + +- normalizza il numero (es. `+39`, `0039`, spazi, trattini) +- confronta le ultime 7 cifre con: + - `telefono_cellulare` + - `telefono_ufficio` + - `telefono_casa` + - `telefono_cellulare_nazionale` + +## 6) Step successivo consigliato + +1. Agganciare evento reale CSTA incoming. +2. Agganciare evento call-ended per aggiornare `esito` e durata. +3. Mostrare popup operatore in Filament con scheda rubrica trovata. diff --git a/docs/ai/restart/START-HERE-NETGESCON-DAY0.md b/docs/ai/restart/START-HERE-NETGESCON-DAY0.md index 6720d47..0b51554 100644 --- a/docs/ai/restart/START-HERE-NETGESCON-DAY0.md +++ b/docs/ai/restart/START-HERE-NETGESCON-DAY0.md @@ -124,7 +124,8 @@ ## Documenti da leggere in ordine (ripartenza agent) 5. `docs/ai/restart/FILAMENT-ONLY-CUTOVER-CHECKLIST.md` 6. `docs/RUNBOOK-DISTRIBUZIONE-REMOTA-NETGESCON.md` 7. `docs/distribuzione-update-netgescon.md` -8. `Miki-Bug-workspace/AGENT_GIT.MD` +8. `docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md` +9. `Miki-Bug-workspace/AGENT_GIT.MD` ## Modifiche chiave gia presenti nel Day-0 diff --git a/routes/api.php b/routes/api.php index 16275ac..c2d35e0 100755 --- a/routes/api.php +++ b/routes/api.php @@ -76,3 +76,12 @@ Route::get('/administrator-routing/{codice}', [\App\Http\Controllers\Api\DistributionController::class, 'getAdministratorRouting']); }); }); + +// --- API CTI Panasonic NS1000 (CSTA) --- +Route::prefix('v1/cti/panasonic')->group(function () { + Route::post('/incoming', [\App\Http\Controllers\Api\PanasonicCstaController::class, 'incoming']) + ->name('api.cti.panasonic.incoming'); + + Route::get('/lookup', [\App\Http\Controllers\Api\PanasonicCstaController::class, 'lookup']) + ->name('api.cti.panasonic.lookup'); +}); diff --git a/scripts/ops/netgescon-panasonic-csta-test.sh b/scripts/ops/netgescon-panasonic-csta-test.sh new file mode 100644 index 0000000..463502a --- /dev/null +++ b/scripts/ops/netgescon-panasonic-csta-test.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple test helper for Panasonic NS1000 CSTA connectivity and webhook simulation. + +PBX_IP="192.168.0.101" +PBX_PORT="33333" +APP_BASE_URL="http://127.0.0.1:8000" +CTI_TOKEN="${NETGESCON_CTI_SHARED_TOKEN:-}" +TEST_PHONE="+393331112233" +TEST_EXT="101" + +usage() { + cat <<'EOF' +Usage: + bash scripts/ops/netgescon-panasonic-csta-test.sh [options] + +Options: + --pbx-ip PBX IP (default: 192.168.0.101) + --pbx-port PBX CSTA port (default: 33333) + --app-url NetGescon base URL (default: http://127.0.0.1:8000) + --token Shared token (or env NETGESCON_CTI_SHARED_TOKEN) + --phone Test caller number + --ext Test called extension + +Example: + bash scripts/ops/netgescon-panasonic-csta-test.sh --token 'my-secret-token' +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --pbx-ip) PBX_IP="$2"; shift 2 ;; + --pbx-port) PBX_PORT="$2"; shift 2 ;; + --app-url) APP_BASE_URL="$2"; shift 2 ;; + --token) CTI_TOKEN="$2"; shift 2 ;; + --phone) TEST_PHONE="$2"; shift 2 ;; + --ext) TEST_EXT="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ -z "$CTI_TOKEN" ]]; then + echo "Missing token: set NETGESCON_CTI_SHARED_TOKEN or pass --token" >&2 + exit 1 +fi + +echo "[1/3] Verifica raggiungibilita CSTA $PBX_IP:$PBX_PORT" +nc -vz "$PBX_IP" "$PBX_PORT" + +echo "[2/3] Lookup numero in rubrica" +curl -sS "${APP_BASE_URL%/}/api/v1/cti/panasonic/lookup?phone=${TEST_PHONE}" \ + -H "X-CTI-Token: ${CTI_TOKEN}" | sed -n '1,120p' + +echo "[3/3] Simulazione evento chiamata in arrivo -> Post-it" +curl -sS -X POST "${APP_BASE_URL%/}/api/v1/cti/panasonic/incoming" \ + -H "Content-Type: application/json" \ + -H "X-CTI-Token: ${CTI_TOKEN}" \ + -d "{\"phone\":\"${TEST_PHONE}\",\"direction\":\"in_arrivo\",\"called_extension\":\"${TEST_EXT}\",\"event_id\":\"test-$(date +%s)\",\"note\":\"test webhook panasonic\"}" | sed -n '1,160p' + +echo "Done"