feat(cti): auto-close post-it on call-ended with duration and listener
This commit is contained in:
parent
4f643c8bef
commit
22be2be817
|
|
@ -96,6 +96,91 @@ public function lookup(Request $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
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', '');
|
||||
|
|
@ -131,6 +216,43 @@ private function findRubricaByPhone(string $phone): ?RubricaUniversale
|
|||
->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 = [
|
||||
|
|
|
|||
|
|
@ -21,15 +21,19 @@ class ChiamataPostIt extends Model
|
|||
'esito',
|
||||
'origine',
|
||||
'origine_id',
|
||||
'durata_secondi',
|
||||
'oggetto',
|
||||
'nota',
|
||||
'priorita',
|
||||
'stato',
|
||||
'chiamata_il',
|
||||
'chiusa_il',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'chiamata_il' => 'datetime',
|
||||
'chiusa_il' => 'datetime',
|
||||
'durata_secondi' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('chiamate_post_it', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('chiamate_post_it', 'durata_secondi')) {
|
||||
$table->unsignedInteger('durata_secondi')->nullable()->after('esito');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('chiamate_post_it', 'chiusa_il')) {
|
||||
$table->timestamp('chiusa_il')->nullable()->after('chiamata_il');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('chiamate_post_it', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('chiamate_post_it', 'chiusa_il')) {
|
||||
$table->dropColumn('chiusa_il');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('chiamate_post_it', 'durata_secondi')) {
|
||||
$table->dropColumn('durata_secondi');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
Obiettivo: intercettare chiamate dal centralino Panasonic NS1000 (CSTA porta 33333) e creare/chiudere automaticamente Post-it in NetGescon con filtro rubrica.
|
||||
|
||||
## Prerequisiti verificati
|
||||
|
||||
|
|
@ -29,12 +29,31 @@ ## 2) Endpoint disponibili
|
|||
- `POST /api/v1/cti/panasonic/incoming`
|
||||
- crea Post-it chiamata in `chiamate_post_it`
|
||||
- fa match automatico numero -> `rubrica_universale`
|
||||
- `POST /api/v1/cti/panasonic/call-ended`
|
||||
- chiude automaticamente il Post-it aperto (match su `event_id`, fallback su numero)
|
||||
- salva `esito`, `durata_secondi`, `chiusa_il`
|
||||
- `GET /api/v1/cti/panasonic/lookup?phone=...`
|
||||
- solo lookup rubrica (match yes/no)
|
||||
|
||||
Autenticazione: header `X-CTI-Token: <TOKEN>`.
|
||||
|
||||
## 3) Test rapido terminale
|
||||
## 3) Comando server web prova (porta 8000)
|
||||
|
||||
```bash
|
||||
cd /home/michele/netgescon/netgescon-day0
|
||||
php artisan serve --host=0.0.0.0 --port=8000
|
||||
```
|
||||
|
||||
## 4) Listener terminale per vedere payload grezzi
|
||||
|
||||
Metti questo listener in ascolto e punta il PBX a `http://<IP_SERVER_200>:8099` per vedere esattamente i dati in arrivo.
|
||||
|
||||
```bash
|
||||
cd /home/michele/netgescon/netgescon-day0
|
||||
bash scripts/ops/netgescon-cti-listener.sh --port 8099
|
||||
```
|
||||
|
||||
## 5) Test rapido terminale (incoming + call-ended)
|
||||
|
||||
```bash
|
||||
cd /home/michele/netgescon/netgescon-day0
|
||||
|
|
@ -44,10 +63,12 @@ ## 3) Test rapido terminale
|
|||
--app-url http://127.0.0.1:8000 \
|
||||
--token "<TOKEN_LUNGO_RANDOM>" \
|
||||
--phone "+393331112233" \
|
||||
--ext "101"
|
||||
--ext "101" \
|
||||
--duration 90 \
|
||||
--outcome "risposta"
|
||||
```
|
||||
|
||||
## 4) Esempio comando da usare lato interfaccia centralino
|
||||
## 6) Esempio comandi da usare lato interfaccia centralino
|
||||
|
||||
Se il centralino puo inviare HTTP su evento incoming call, usa:
|
||||
|
||||
|
|
@ -72,7 +93,23 @@ ## 4) Esempio comando da usare lato interfaccia centralino
|
|||
-H "X-CTI-Token: <TOKEN_LUNGO_RANDOM>"
|
||||
```
|
||||
|
||||
## 5) Cosa fa il match numeri
|
||||
Per evento fine chiamata (chiusura automatica Post-it):
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "http://<IP_NETGESCON>:8000/api/v1/cti/panasonic/call-ended" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CTI-Token: <TOKEN_LUNGO_RANDOM>" \
|
||||
-d '{
|
||||
"phone": "<CALLING_NUMBER>",
|
||||
"event_id": "<PBX_EVENT_ID>",
|
||||
"outcome": "risposta",
|
||||
"duration_seconds": 83,
|
||||
"ended_at": "2026-03-11T10:20:00+01:00",
|
||||
"note": "evento call-ended NS1000"
|
||||
}'
|
||||
```
|
||||
|
||||
## 7) Cosa fa il match numeri
|
||||
|
||||
- normalizza il numero (es. `+39`, `0039`, spazi, trattini)
|
||||
- confronta le ultime 7 cifre con:
|
||||
|
|
@ -81,7 +118,26 @@ ## 5) Cosa fa il match numeri
|
|||
- `telefono_casa`
|
||||
- `telefono_cellulare_nazionale`
|
||||
|
||||
## 6) Step successivo consigliato
|
||||
## 8) Dev + staging sulla stessa macchina (.200)
|
||||
|
||||
- Sviluppo consigliato: `http://127.0.0.1:8000`
|
||||
- Staging consigliato: `http://127.0.0.1:8001`
|
||||
|
||||
Comandi esempio:
|
||||
|
||||
```bash
|
||||
# sviluppo
|
||||
cd /home/michele/netgescon/netgescon-day0
|
||||
php artisan serve --host=0.0.0.0 --port=8000
|
||||
|
||||
# staging (altro terminale, altra working copy o env staging)
|
||||
cd /home/michele/netgescon/netgescon-day0
|
||||
APP_ENV=staging php artisan serve --host=0.0.0.0 --port=8001
|
||||
```
|
||||
|
||||
Nel PBX configura URL diversi per ambiente, mantenendo stesso payload/token o token distinti.
|
||||
|
||||
## 9) Step successivo consigliato
|
||||
|
||||
1. Agganciare evento reale CSTA incoming.
|
||||
2. Agganciare evento call-ended per aggiornare `esito` e durata.
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@
|
|||
Route::post('/incoming', [\App\Http\Controllers\Api\PanasonicCstaController::class, 'incoming'])
|
||||
->name('api.cti.panasonic.incoming');
|
||||
|
||||
Route::post('/call-ended', [\App\Http\Controllers\Api\PanasonicCstaController::class, 'callEnded'])
|
||||
->name('api.cti.panasonic.call_ended');
|
||||
|
||||
Route::get('/lookup', [\App\Http\Controllers\Api\PanasonicCstaController::class, 'lookup'])
|
||||
->name('api.cti.panasonic.lookup');
|
||||
});
|
||||
|
|
|
|||
59
scripts/ops/netgescon-cti-listener.sh
Executable file
59
scripts/ops/netgescon-cti-listener.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Minimal raw HTTP listener to inspect payload sent by PBX/webhook tools.
|
||||
# Use this only for diagnostics in a trusted network.
|
||||
|
||||
PORT="8099"
|
||||
OUT_DIR="storage/logs"
|
||||
OUT_FILE=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/ops/netgescon-cti-listener.sh [options]
|
||||
|
||||
Options:
|
||||
--port <port> Local listen port (default: 8099)
|
||||
--out <file> Output file (default: storage/logs/cti-listener-<timestamp>.log)
|
||||
|
||||
Examples:
|
||||
bash scripts/ops/netgescon-cti-listener.sh
|
||||
bash scripts/ops/netgescon-cti-listener.sh --port 8099 --out storage/logs/cti-live.log
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--out) OUT_FILE="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUT_FILE" ]]; then
|
||||
mkdir -p "$OUT_DIR"
|
||||
OUT_FILE="$OUT_DIR/cti-listener-$(date +%Y%m%d-%H%M%S).log"
|
||||
fi
|
||||
|
||||
if command -v ncat >/dev/null 2>&1; then
|
||||
echo "[listener] ncat in ascolto su 0.0.0.0:${PORT}"
|
||||
echo "[listener] output: ${OUT_FILE}"
|
||||
echo "[listener] stop: Ctrl+C"
|
||||
ncat -kl "$PORT" | tee -a "$OUT_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v nc >/dev/null 2>&1; then
|
||||
echo "[listener] nc in ascolto su 0.0.0.0:${PORT}"
|
||||
echo "[listener] output: ${OUT_FILE}"
|
||||
echo "[listener] stop: Ctrl+C"
|
||||
while true; do
|
||||
nc -l -p "$PORT" | tee -a "$OUT_FILE"
|
||||
printf '\n----- END REQUEST %s -----\n' "$(date -Iseconds)" | tee -a "$OUT_FILE"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Missing netcat tool. Install ncat or nc first." >&2
|
||||
exit 1
|
||||
18
scripts/ops/netgescon-panasonic-csta-test.sh
Normal file → Executable file
18
scripts/ops/netgescon-panasonic-csta-test.sh
Normal file → Executable file
|
|
@ -9,6 +9,8 @@ APP_BASE_URL="http://127.0.0.1:8000"
|
|||
CTI_TOKEN="${NETGESCON_CTI_SHARED_TOKEN:-}"
|
||||
TEST_PHONE="+393331112233"
|
||||
TEST_EXT="101"
|
||||
TEST_DURATION="75"
|
||||
TEST_OUTCOME="risposta"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
|
|
@ -22,6 +24,8 @@ Options:
|
|||
--token <token> Shared token (or env NETGESCON_CTI_SHARED_TOKEN)
|
||||
--phone <phone> Test caller number
|
||||
--ext <ext> Test called extension
|
||||
--duration <sec> Test call duration in seconds (default: 75)
|
||||
--outcome <text> Test call outcome (default: risposta)
|
||||
|
||||
Example:
|
||||
bash scripts/ops/netgescon-panasonic-csta-test.sh --token 'my-secret-token'
|
||||
|
|
@ -36,6 +40,8 @@ while [[ $# -gt 0 ]]; do
|
|||
--token) CTI_TOKEN="$2"; shift 2 ;;
|
||||
--phone) TEST_PHONE="$2"; shift 2 ;;
|
||||
--ext) TEST_EXT="$2"; shift 2 ;;
|
||||
--duration) TEST_DURATION="$2"; shift 2 ;;
|
||||
--outcome) TEST_OUTCOME="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
|
|
@ -53,10 +59,18 @@ 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"
|
||||
EVENT_ID="test-$(date +%s)"
|
||||
|
||||
echo "[3/4] 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'
|
||||
-d "{\"phone\":\"${TEST_PHONE}\",\"direction\":\"in_arrivo\",\"called_extension\":\"${TEST_EXT}\",\"event_id\":\"${EVENT_ID}\",\"note\":\"test webhook panasonic incoming\"}" | sed -n '1,160p'
|
||||
|
||||
echo "[4/4] Simulazione evento call-ended -> chiusura automatica Post-it"
|
||||
curl -sS -X POST "${APP_BASE_URL%/}/api/v1/cti/panasonic/call-ended" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CTI-Token: ${CTI_TOKEN}" \
|
||||
-d "{\"phone\":\"${TEST_PHONE}\",\"event_id\":\"${EVENT_ID}\",\"outcome\":\"${TEST_OUTCOME}\",\"duration_seconds\":${TEST_DURATION},\"ended_at\":\"$(date -Iseconds)\",\"note\":\"test webhook panasonic call-ended\"}" | sed -n '1,200p'
|
||||
|
||||
echo "Done"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user