feat(cti): add Panasonic NS1000 webhook+lookup and restart runbook
This commit is contained in:
parent
44438093a9
commit
4f643c8bef
154
app/Http/Controllers/Api/PanasonicCstaController.php
Normal file
154
app/Http/Controllers/Api/PanasonicCstaController.php
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ChiamataPostIt;
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Support\PhoneNumber;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class PanasonicCstaController extends Controller
|
||||||
|
{
|
||||||
|
public function incoming(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (! $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md
Normal file
88
docs/ai/restart/CTI-PANASONIC-NS1000-SETUP.md
Normal file
|
|
@ -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=<TOKEN_LUNGO_RANDOM>
|
||||||
|
```
|
||||||
|
|
||||||
|
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: <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 "<TOKEN_LUNGO_RANDOM>" \
|
||||||
|
--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://<IP_NETGESCON>:8000/api/v1/cti/panasonic/incoming" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-CTI-Token: <TOKEN_LUNGO_RANDOM>" \
|
||||||
|
-d '{
|
||||||
|
"phone": "<CALLING_NUMBER>",
|
||||||
|
"name": "<CALLER_NAME_OPTIONAL>",
|
||||||
|
"direction": "in_arrivo",
|
||||||
|
"called_extension": "<EXT>",
|
||||||
|
"event_id": "<PBX_EVENT_ID>",
|
||||||
|
"note": "evento CSTA NS1000"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Per filtro rubrica (prima del popup interno):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "http://<IP_NETGESCON>:8000/api/v1/cti/panasonic/lookup?phone=<CALLING_NUMBER>" \
|
||||||
|
-H "X-CTI-Token: <TOKEN_LUNGO_RANDOM>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
@ -124,7 +124,8 @@ ## Documenti da leggere in ordine (ripartenza agent)
|
||||||
5. `docs/ai/restart/FILAMENT-ONLY-CUTOVER-CHECKLIST.md`
|
5. `docs/ai/restart/FILAMENT-ONLY-CUTOVER-CHECKLIST.md`
|
||||||
6. `docs/RUNBOOK-DISTRIBUZIONE-REMOTA-NETGESCON.md`
|
6. `docs/RUNBOOK-DISTRIBUZIONE-REMOTA-NETGESCON.md`
|
||||||
7. `docs/distribuzione-update-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
|
## Modifiche chiave gia presenti nel Day-0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,12 @@
|
||||||
Route::get('/administrator-routing/{codice}', [\App\Http\Controllers\Api\DistributionController::class, 'getAdministratorRouting']);
|
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');
|
||||||
|
});
|
||||||
|
|
|
||||||
62
scripts/ops/netgescon-panasonic-csta-test.sh
Normal file
62
scripts/ops/netgescon-panasonic-csta-test.sh
Normal file
|
|
@ -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 <ip> PBX IP (default: 192.168.0.101)
|
||||||
|
--pbx-port <port> PBX CSTA port (default: 33333)
|
||||||
|
--app-url <url> NetGescon base URL (default: http://127.0.0.1:8000)
|
||||||
|
--token <token> Shared token (or env NETGESCON_CTI_SHARED_TOKEN)
|
||||||
|
--phone <phone> Test caller number
|
||||||
|
--ext <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"
|
||||||
Loading…
Reference in New Issue
Block a user