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 Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class PanasonicCstaController extends Controller
|
||||
|
|
@ -53,6 +55,8 @@ public function incoming(Request $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
$this->mirrorToPeerIfEnabled($request, 'incoming', $payload);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'source' => 'panasonic_ns1000',
|
||||
|
|
@ -169,6 +173,8 @@ public function callEnded(Request $request): JsonResponse
|
|||
|
||||
$postIt->save();
|
||||
|
||||
$this->mirrorToPeerIfEnabled($request, 'call-ended', $payload);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'source' => 'panasonic_ns1000',
|
||||
|
|
@ -182,6 +188,19 @@ public function callEnded(Request $request): JsonResponse
|
|||
}
|
||||
|
||||
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', '');
|
||||
if ($configured === '') {
|
||||
|
|
@ -200,13 +219,47 @@ private function isAuthorized(Request $request): bool
|
|||
$configured = (string) env('CTI_PANASONIC_TOKEN', '');
|
||||
}
|
||||
|
||||
if ($configured === '') {
|
||||
return false;
|
||||
return $configured;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$token = (string) ($request->header('X-CTI-Token') ?: $request->input('token', ''));
|
||||
if ((string) $request->header('X-CTI-Mirrored', '0') === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
return hash_equals($configured, $token);
|
||||
$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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Console\Commands\DedupeRateEmesseCommand;
|
||||
use App\Console\Commands\CtiSyncPostItFromStagingCommand;
|
||||
use App\Console\Commands\FeCassettoImportLocal;
|
||||
use App\Console\Commands\GesconCollegaFornitoreStabili;
|
||||
use App\Console\Commands\GesconImportAlignCommand;
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
GoogleImportKeepNotesCommand::class,
|
||||
GoogleSyncTicketCalendarCommand::class,
|
||||
GoogleSyncFornitoriRubricaCommand::class,
|
||||
CtiSyncPostItFromStagingCommand::class,
|
||||
])
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$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 | 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 / 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