netgescon-day0/app/Services/Fornitore/FornitoreRubricaSyncService.php

427 lines
17 KiB
PHP

<?php
namespace App\Services\Fornitore;
use App\Models\AssistenzaTecnorepairScheda;
use App\Models\Fornitore;
use App\Models\FornitoreCliente;
use App\Models\RubricaUniversale;
use App\Models\TicketIntervento;
use App\Services\Tecnorepair\TecnoRepairArchiveService;
use App\Support\PhoneNumber;
use App\Support\TecnoRepairMdbReader;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class FornitoreRubricaSyncService
{
public function __construct(
private readonly TecnoRepairMdbReader $mdbReader,
private readonly TecnoRepairArchiveService $archiveService,
) {
}
/**
* Synchronize contacts from all three sources:
* 1. TecnoRepair MDB (TClienti)
* 2. Contabilità MySQL arc_nehr (cli + ind)
* 3. NetGescon Tickets / Stabili
*
* @return array{tecnorepair: int, contabilita: int, tickets: int, total: int}
*/
public function syncAll(int $amministratoreId, int $fornitoreId): array
{
$primaryFornitoreId = in_array($fornitoreId, [236, 359], true) ? 236 : $fornitoreId;
$stats = [
'tecnorepair' => 0,
'contabilita' => 0,
'tickets' => 0,
'total' => 0,
];
// 1. Sync TecnoRepair TClienti
try {
$stats['tecnorepair'] = $this->syncTecnoRepair($amministratoreId, $primaryFornitoreId);
} catch (\Throwable $e) {
Log::warning('FornitoreRubricaSyncService: errore sync TecnoRepair: ' . $e->getMessage());
}
// 2. Sync Contabilità MySQL
try {
$stats['contabilita'] = $this->syncContabilita($amministratoreId, $primaryFornitoreId);
} catch (\Throwable $e) {
Log::warning('FornitoreRubricaSyncService: errore sync Contabilità: ' . $e->getMessage());
}
// 3. Sync NetGescon Tickets
try {
$stats['tickets'] = $this->syncNetGesconTickets($amministratoreId, $primaryFornitoreId);
} catch (\Throwable $e) {
Log::warning('FornitoreRubricaSyncService: errore sync Tickets: ' . $e->getMessage());
}
$stats['total'] = FornitoreCliente::query()
->where('fornitore_id', $primaryFornitoreId)
->count();
return $stats;
}
/**
* Import contacts from TecnoRepairDB.mdb TClienti table
*/
public function syncTecnoRepair(int $amministratoreId, int $fornitoreId): int
{
$fornitore = Fornitore::query()->find($fornitoreId);
$mdbPath = $this->archiveService->resolveMdbPath(null, $fornitore);
if (! file_exists($mdbPath) || ! is_readable($mdbPath)) {
return 0;
}
$tables = $this->mdbReader->listTables($mdbPath);
if (! in_array('TClienti', $tables, true)) {
return 0;
}
$clientiRows = $this->mdbReader->exportTable($mdbPath, 'TClienti');
$count = 0;
foreach ($clientiRows as $row) {
$legacyId = isset($row['ID']) && is_numeric($row['ID']) ? (int) $row['ID'] : null;
$name = trim((string) ($row['NomeCognome'] ?? ''));
if (! $legacyId && $name === '') {
continue;
}
$phone = trim((string) ($row['NumeroTelefono'] ?? ''));
$phoneAlt = trim((string) ($row['TelFisso'] ?? ''));
$email = trim((string) ($row['Email'] ?? ''));
$cf = trim((string) ($row['CodFis'] ?? ''));
$piva = trim((string) ($row['PIVA'] ?? ''));
// Link to rubrica_universale if found
$rubricaId = $this->findRubricaMatch($amministratoreId, $cf, $piva, $phone, $phoneAlt, $email, $name);
FornitoreCliente::query()->updateOrCreate(
[
'fornitore_id' => $fornitoreId,
'legacy_cliente_id' => $legacyId,
],
[
'amministratore_id' => $amministratoreId,
'rubrica_id' => $rubricaId,
'display_name' => $name ?: ('Cliente TecnoRepair #' . $legacyId),
'phone' => $phone,
'phone_alt' => $phoneAlt,
'email' => $email,
'indirizzo' => trim((string) ($row['Indirizzo'] ?? '')),
'cap' => trim((string) ($row['Cap'] ?? '')),
'citta' => trim((string) ($row['Citta'] ?? '')),
'provincia' => trim((string) ($row['Prov'] ?? '')),
'partita_iva' => $piva,
'codice_fiscale' => $cf,
'note' => trim((string) ($row['Annotazioni'] ?? '')),
'source' => 'tecnorepair_tclienti',
'imported_from_path' => $mdbPath,
'imported_at' => now(),
'metadata' => [
'raw' => $row,
],
]
);
$count++;
}
return $count;
}
/**
* Import contacts from Contabilità MySQL (arc_nehr: cli + ind)
*/
public function syncContabilita(int $amministratoreId, int $fornitoreId): int
{
$conn = DB::connection('contabilita_mysql');
// Check if connection works
try {
$conn->getPdo();
} catch (\Throwable $e) {
Log::info('Contabilità MySQL non disponibile per sync rubrica: ' . $e->getMessage());
return 0;
}
// Read destinations/addresses with phone numbers from `ind`
$indRows = $conn->table('ind')
->where(function ($q) {
$q->where('TELEFONO', '!=', '')
->orWhere('CELLULARE', '!=', '')
->orWhere('e_mail', '!=', '');
})
->get();
$indByCli = [];
foreach ($indRows as $ind) {
$cliCod = trim((string) $ind->CLI_CODICE);
if ($cliCod !== '' && ! isset($indByCli[$cliCod])) {
$indByCli[$cliCod] = $ind;
}
}
// Read all active customers from `cli`
$cliList = $conn->table('cli')
->where('OBSOLETO', '!=', 'si')
->get([
'ID', 'CODICE', 'DESCRIZIONE1', 'DESCRIZIONE2', 'VIA', 'CITTA',
'PARTITA_IVA', 'CODICE_FISCALE', 'NOTE'
]);
$count = 0;
foreach ($cliList as $cli) {
$desc1 = trim((string) $cli->DESCRIZIONE1);
$desc2 = trim((string) $cli->DESCRIZIONE2);
$name = trim($desc1 . ' ' . $desc2);
if ($name === '') {
continue;
}
$codice = trim((string) $cli->CODICE);
$ind = $indByCli[$codice] ?? null;
$phone = trim((string) ($ind?->CELLULARE ?: $ind?->TELEFONO ?: ''));
$phoneAlt = trim((string) ($ind?->TELEFONO_01 ?: $ind?->FAX ?: ''));
$email = trim((string) ($ind?->e_mail ?: ''));
$piva = trim((string) ($cli->PARTITA_IVA ?: $ind?->partita_iva ?: ''));
$cf = trim((string) ($cli->CODICE_FISCALE ?: $ind?->codice_fiscale ?: ''));
// Check if already present from TecnoRepair
$existing = FornitoreCliente::query()
->where('fornitore_id', $fornitoreId)
->where(function ($q) use ($name, $cf, $piva, $codice) {
if ($cf !== '') {
$q->orWhere('codice_fiscale', $cf);
}
if ($piva !== '') {
$q->orWhere('partita_iva', $piva);
}
$q->orWhere('metadata->cli_codice', $codice);
})
->first();
if ($existing) {
// Enrich existing record with contabilità metadata
$meta = is_array($existing->metadata) ? $existing->metadata : [];
$meta['cli_codice'] = $codice;
$meta['contabilita_sync'] = true;
$existing->metadata = $meta;
if ($existing->phone === '' && $phone !== '') {
$existing->phone = $phone;
}
if ($existing->email === '' && $email !== '') {
$existing->email = $email;
}
$existing->save();
$count++;
continue;
}
$rubricaId = $this->findRubricaMatch($amministratoreId, $cf, $piva, $phone, $phoneAlt, $email, $name);
FornitoreCliente::query()->create([
'amministratore_id' => $amministratoreId,
'fornitore_id' => $fornitoreId,
'rubrica_id' => $rubricaId,
'legacy_cliente_id' => null,
'display_name' => $name,
'phone' => $phone,
'phone_alt' => $phoneAlt,
'email' => $email,
'indirizzo' => trim((string) ($cli->VIA ?: $ind?->VIA ?: '')),
'cap' => trim((string) ($ind?->CAP ?: '')),
'citta' => trim((string) ($cli->CITTA ?: $ind?->CITTA ?: '')),
'provincia' => trim((string) ($ind?->provincia ?: '')),
'partita_iva' => $piva,
'codice_fiscale' => $cf,
'note' => trim((string) ($cli->NOTE ?: $ind?->NOTE ?: '')),
'source' => 'contabilita_mysql',
'imported_from_path' => 'arc_nehr.cli',
'imported_at' => now(),
'metadata' => [
'cli_codice' => $codice,
'ind_id' => $ind?->ID ?? null,
],
]);
$count++;
}
return $count;
}
/**
* Sync contacts from NetGescon tickets assigned to supplier
*/
public function syncNetGesconTickets(int $amministratoreId, int $fornitoreId): int
{
$supplierIds = in_array($fornitoreId, [236, 359], true) ? [236, 359, 392] : [$fornitoreId];
$interventi = TicketIntervento::query()
->with(['ticket.stabile', 'ticket.unitaImmobiliare', 'ticket.soggettoRichiedente'])
->whereIn('fornitore_id', $supplierIds)
->latest('created_at')
->limit(200)
->get();
$count = 0;
foreach ($interventi as $intervento) {
$ticket = $intervento->ticket;
if (! $ticket) {
continue;
}
$soggetto = $ticket->soggettoRichiedente;
$name = '';
$phone = '';
$email = '';
$cf = '';
if ($soggetto) {
$name = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? ''))));
$phone = trim((string) ($soggetto->telefono ?: ''));
$email = trim((string) ($soggetto->email ?: ''));
$cf = trim((string) ($soggetto->codice_fiscale ?: ''));
}
if ($name === '' && $ticket->descrizione) {
// Extract from description lines
if (preg_match('/(?:Chiamante selezionato|Contatto associato):\s*(.+)/i', $ticket->descrizione, $m)) {
$name = trim($m[1]);
}
if (preg_match('/(?:Telefono|Telefono richiamabile):\s*([0-9+\s().\/-]+)/i', $ticket->descrizione, $m)) {
$phone = trim($m[1]);
}
}
if ($name === '' && $phone === '') {
continue;
}
$name = $name ?: ('Richiedente Ticket #' . $ticket->id);
// Check if already present
$existing = FornitoreCliente::query()
->where('fornitore_id', $fornitoreId)
->where(function ($q) use ($name, $phone, $cf) {
if ($cf !== '') {
$q->where('codice_fiscale', $cf);
} elseif ($phone !== '') {
$norm = PhoneNumber::normalizeForMatch($phone);
$q->whereRaw("REGEXP_REPLACE(COALESCE(phone, ''), '[^0-9]', '') = ?", [$norm]);
} else {
$q->where('display_name', $name);
}
})
->first();
if ($existing) {
continue;
}
$rubricaId = $this->findRubricaMatch($amministratoreId, $cf, null, $phone, null, $email, $name);
FornitoreCliente::query()->create([
'amministratore_id' => $amministratoreId,
'fornitore_id' => $fornitoreId,
'rubrica_id' => $rubricaId,
'legacy_cliente_id' => null,
'display_name' => $name,
'phone' => $phone,
'phone_alt' => '',
'email' => $email,
'indirizzo' => (string) ($ticket->unitaImmobiliare?->scala_piano_interno ?? ''),
'cap' => '',
'citta' => (string) ($ticket->stabile?->citta ?? ''),
'provincia' => (string) ($ticket->stabile?->provincia ?? ''),
'partita_iva' => '',
'codice_fiscale' => $cf,
'note' => 'Ticket #' . $ticket->id . ': ' . ($ticket->titolo ?: '-'),
'source' => 'netgescon_ticket',
'imported_from_path' => 'tickets.id#' . $ticket->id,
'imported_at' => now(),
'metadata' => [
'ticket_id' => (int) $ticket->id,
'stabile_nome' => (string) ($ticket->stabile?->denominazione ?? ''),
'stabile_codice' => (string) ($ticket->stabile?->cod_stabile ?? ''),
],
]);
$count++;
}
return $count;
}
private function findRubricaMatch(
int $amministratoreId,
?string $cf = null,
?string $piva = null,
?string $phone = null,
?string $phoneAlt = null,
?string $email = null,
?string $name = null
): ?int {
$query = RubricaUniversale::query()->where('amministratore_id', $amministratoreId);
if ($cf !== null && $cf !== '') {
$m = (clone $query)->where('codice_fiscale', strtoupper(trim($cf)))->value('id');
if ($m) return (int) $m;
}
if ($piva !== null && $piva !== '') {
$m = (clone $query)->where('partita_iva', trim($piva))->value('id');
if ($m) return (int) $m;
}
if ($phone !== null && trim($phone) !== '') {
$digits = PhoneNumber::normalizeForMatch($phone);
if ($digits !== '') {
$m = (clone $query)->where(function ($bq) use ($digits) {
$bq->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$digits])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$digits])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$digits]);
})->value('id');
if ($m) return (int) $m;
}
}
if ($phoneAlt !== null && trim($phoneAlt) !== '') {
$digits = PhoneNumber::normalizeForMatch($phoneAlt);
if ($digits !== '') {
$m = (clone $query)->where(function ($bq) use ($digits) {
$bq->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$digits])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$digits]);
})->value('id');
if ($m) return (int) $m;
}
}
if ($email !== null && trim($email) !== '') {
$m = (clone $query)->whereRaw('LOWER(email) = ?', [mb_strtolower(trim($email))])->value('id');
if ($m) return (int) $m;
}
if ($name !== null && trim($name) !== '') {
$m = (clone $query)->where(function ($nq) use ($name) {
$nq->whereRaw("LOWER(TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))) = ?", [mb_strtolower(trim($name))])
->orWhereRaw("LOWER(TRIM(COALESCE(ragione_sociale, ''))) = ?", [mb_strtolower(trim($name))]);
})->value('id');
if ($m) return (int) $m;
}
return null;
}
}