CTI Panasonic: mirror staging->dev e comando sync unidirezionale
This commit is contained in:
parent
3ce1113cd2
commit
11f078b8b2
127
app/Console/Commands/CtiSyncPostItFromStagingCommand.php
Normal file
127
app/Console/Commands/CtiSyncPostItFromStagingCommand.php
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CtiSyncPostItFromStagingCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cti:sync-postit-from-staging
|
||||||
|
{--from-connection=staging_mysql : Connessione sorgente (staging)}
|
||||||
|
{--to-connection=mysql : Connessione destinazione (sviluppo)}
|
||||||
|
{--since-id=0 : Sincronizza record con id maggiore di questo valore}
|
||||||
|
{--limit=2000 : Numero massimo record da leggere per esecuzione}
|
||||||
|
{--replace : Rimuove prima i record Panasonic in destinazione}
|
||||||
|
{--apply : Applica realmente (senza flag e\' dry-run)}';
|
||||||
|
|
||||||
|
protected $description = 'Sincronizza chiamate_post_it Panasonic da staging a sviluppo in modo unidirezionale.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$fromConnection = (string) $this->option('from-connection');
|
||||||
|
$toConnection = (string) $this->option('to-connection');
|
||||||
|
$sinceId = max(0, (int) $this->option('since-id'));
|
||||||
|
$limit = max(1, min(20000, (int) $this->option('limit')));
|
||||||
|
$replace = (bool) $this->option('replace');
|
||||||
|
$apply = (bool) $this->option('apply');
|
||||||
|
|
||||||
|
if (! Config::has('database.connections.' . $fromConnection)) {
|
||||||
|
$this->error("Connessione sorgente non definita: {$fromConnection}");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Config::has('database.connections.' . $toConnection)) {
|
||||||
|
$this->error("Connessione destinazione non definita: {$toConnection}");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fromConnection === $toConnection) {
|
||||||
|
$this->error('Connessione sorgente e destinazione coincidono. Interrompo per sicurezza.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fromCfg = (array) config('database.connections.' . $fromConnection, []);
|
||||||
|
$toCfg = (array) config('database.connections.' . $toConnection, []);
|
||||||
|
$samePhysicalDb =
|
||||||
|
strtolower((string) ($fromCfg['host'] ?? '')) === strtolower((string) ($toCfg['host'] ?? '')) &&
|
||||||
|
(string) ($fromCfg['port'] ?? '') === (string) ($toCfg['port'] ?? '') &&
|
||||||
|
strtolower((string) ($fromCfg['database'] ?? '')) === strtolower((string) ($toCfg['database'] ?? '')) &&
|
||||||
|
strtolower((string) ($fromCfg['username'] ?? '')) === strtolower((string) ($toCfg['username'] ?? '')) &&
|
||||||
|
(string) ($fromCfg['unix_socket'] ?? '') === (string) ($toCfg['unix_socket'] ?? '');
|
||||||
|
|
||||||
|
if ($samePhysicalDb) {
|
||||||
|
$this->error('Sorgente e destinazione puntano allo stesso DB fisico. Interrompo per sicurezza.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::connection($fromConnection)->hasTable('chiamate_post_it')) {
|
||||||
|
$this->error('Tabella chiamate_post_it assente sulla connessione sorgente.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::connection($toConnection)->hasTable('chiamate_post_it')) {
|
||||||
|
$this->error('Tabella chiamate_post_it assente sulla connessione destinazione.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceColumns = Schema::connection($fromConnection)->getColumnListing('chiamate_post_it');
|
||||||
|
$targetColumns = Schema::connection($toConnection)->getColumnListing('chiamate_post_it');
|
||||||
|
$columns = array_values(array_intersect($sourceColumns, $targetColumns));
|
||||||
|
|
||||||
|
if (! in_array('id', $columns, true)) {
|
||||||
|
$this->error('La tabella destinazione deve avere la colonna id per l\'upsert.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = DB::connection($fromConnection)
|
||||||
|
->table('chiamate_post_it')
|
||||||
|
->where('origine', 'panasonic_ns1000')
|
||||||
|
->where('id', '>', $sinceId)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($limit)
|
||||||
|
->get($columns);
|
||||||
|
|
||||||
|
if ($rows->isEmpty()) {
|
||||||
|
$this->info('Nessun record Panasonic da sincronizzare.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstId = (int) ($rows->first()->id ?? 0);
|
||||||
|
$lastId = (int) ($rows->last()->id ?? 0);
|
||||||
|
$this->line("Record letti: {$rows->count()} (id {$firstId} -> {$lastId})");
|
||||||
|
$this->line("Direzione: {$fromConnection} -> {$toConnection}");
|
||||||
|
|
||||||
|
if (! $apply) {
|
||||||
|
$this->warn('Dry-run attivo. Usa --apply per eseguire la sincronizzazione.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $rows
|
||||||
|
->map(fn ($row): array => Arr::only((array) $row, $columns))
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$updateColumns = array_values(array_filter($columns, fn (string $col): bool => $col !== 'id'));
|
||||||
|
|
||||||
|
DB::connection($toConnection)->transaction(function () use ($toConnection, $replace, $payload, $updateColumns): void {
|
||||||
|
$query = DB::connection($toConnection)->table('chiamate_post_it');
|
||||||
|
|
||||||
|
if ($replace) {
|
||||||
|
$deleted = $query->where('origine', 'panasonic_ns1000')->delete();
|
||||||
|
$this->line("Record Panasonic rimossi in destinazione: {$deleted}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_chunk($payload, 500) as $chunk) {
|
||||||
|
DB::connection($toConnection)
|
||||||
|
->table('chiamate_post_it')
|
||||||
|
->upsert($chunk, ['id'], $updateColumns);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info('Sincronizzazione completata con successo.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
use App\Support\PhoneNumber;
|
use App\Support\PhoneNumber;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class PanasonicCstaController extends Controller
|
class PanasonicCstaController extends Controller
|
||||||
|
|
@ -53,6 +55,8 @@ public function incoming(Request $request): JsonResponse
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->mirrorToPeerIfEnabled($request, 'incoming', $payload);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'source' => 'panasonic_ns1000',
|
'source' => 'panasonic_ns1000',
|
||||||
|
|
@ -169,6 +173,8 @@ public function callEnded(Request $request): JsonResponse
|
||||||
|
|
||||||
$postIt->save();
|
$postIt->save();
|
||||||
|
|
||||||
|
$this->mirrorToPeerIfEnabled($request, 'call-ended', $payload);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'source' => 'panasonic_ns1000',
|
'source' => 'panasonic_ns1000',
|
||||||
|
|
@ -182,6 +188,19 @@ public function callEnded(Request $request): JsonResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isAuthorized(Request $request): bool
|
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 = (string) env('NETGESCON_CTI_SHARED_TOKEN', '');
|
$configured = (string) env('NETGESCON_CTI_SHARED_TOKEN', '');
|
||||||
if ($configured === '') {
|
if ($configured === '') {
|
||||||
|
|
@ -200,13 +219,47 @@ private function isAuthorized(Request $request): bool
|
||||||
$configured = (string) env('CTI_PANASONIC_TOKEN', '');
|
$configured = (string) env('CTI_PANASONIC_TOKEN', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($configured === '') {
|
return $configured;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = (string) ($request->header('X-CTI-Token') ?: $request->input('token', ''));
|
private function mirrorToPeerIfEnabled(Request $request, string $endpoint, array $payload): void
|
||||||
|
{
|
||||||
|
$enabled = filter_var((string) env('NETGESCON_CTI_MIRROR_ENABLED', 'false'), FILTER_VALIDATE_BOOL);
|
||||||
|
if (! $enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return hash_equals($configured, $token);
|
if ((string) $request->header('X-CTI-Mirrored', '0') === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = rtrim((string) env('NETGESCON_CTI_MIRROR_BASE_URL', ''), '/');
|
||||||
|
if ($baseUrl === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->resolveConfiguredToken();
|
||||||
|
if ($token === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timeout = (int) env('NETGESCON_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
|
private function findRubricaByPhone(string $phone): ?RubricaUniversale
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Console\Commands\DedupeRateEmesseCommand;
|
use App\Console\Commands\DedupeRateEmesseCommand;
|
||||||
|
use App\Console\Commands\CtiSyncPostItFromStagingCommand;
|
||||||
use App\Console\Commands\FeCassettoImportLocal;
|
use App\Console\Commands\FeCassettoImportLocal;
|
||||||
use App\Console\Commands\GesconCollegaFornitoreStabili;
|
use App\Console\Commands\GesconCollegaFornitoreStabili;
|
||||||
use App\Console\Commands\GesconImportAlignCommand;
|
use App\Console\Commands\GesconImportAlignCommand;
|
||||||
|
|
@ -57,6 +58,7 @@
|
||||||
GoogleImportKeepNotesCommand::class,
|
GoogleImportKeepNotesCommand::class,
|
||||||
GoogleSyncTicketCalendarCommand::class,
|
GoogleSyncTicketCalendarCommand::class,
|
||||||
GoogleSyncFornitoriRubricaCommand::class,
|
GoogleSyncFornitoriRubricaCommand::class,
|
||||||
|
CtiSyncPostItFromStagingCommand::class,
|
||||||
])
|
])
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
$middleware->append(\App\Http\Middleware\ForceFilamentUiMiddleware::class);
|
$middleware->append(\App\Http\Middleware\ForceFilamentUiMiddleware::class);
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,4 @@ # Modifiche NetGescon
|
||||||
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / Post-It | Nuovi endpoint incoming, lookup e call-ended con chiusura automatica Post-it, salvataggio esito e durata chiamata | [U] |
|
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / Post-It | Nuovi endpoint incoming, lookup e call-ended con chiusura automatica Post-it, salvataggio esito e durata chiamata | [U] |
|
||||||
| 11/03/2026 | 0.8.x-dev | Day-0 / Staging / Operativita | Handoff operativo completo, comandi diagnostica payload CSTA/HTTP e allineamento staging nginx su /var/www/netgescon:80 | [P] |
|
| 11/03/2026 | 0.8.x-dev | Day-0 / Staging / Operativita | Handoff operativo completo, comandi diagnostica payload CSTA/HTTP e allineamento staging nginx su /var/www/netgescon:80 | [P] |
|
||||||
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / DB condiviso | Verificato allineamento DB dev/staging su base unica, applicata migrazione campi `durata_secondi`/`chiusa_il`, fallback token CTI multi-chiave e test end-to-end incoming/call-ended/lookup con chiusura Post-it validata | [U] |
|
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / DB condiviso | Verificato allineamento DB dev/staging su base unica, applicata migrazione campi `durata_secondi`/`chiusa_il`, fallback token CTI multi-chiave e test end-to-end incoming/call-ended/lookup con chiusura Post-it validata | [U] |
|
||||||
|
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / Sync staging->sviluppo | Introdotti mirror payload CTI opzionale (solo staging) e comando `cti:sync-postit-from-staging` per riallineamento unidirezionale record `chiamate_post_it` Panasonic verso sviluppo | [U] |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user