netgescon-day0/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php

441 lines
18 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use App\Models\Amministratore;
use App\Models\AssistenzaTecnorepairScheda;
use App\Models\Fornitore;
use App\Models\FornitoreCliente;
use App\Models\RubricaUniversale;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class TecnoRepairImportRubricaClientiCommand extends Command
{
protected $signature = 'tecnorepair:import-rubrica-clienti
{amministratore : ID o codice amministratore}
{--fornitore-id= : ID fornitore da collegare ai clienti}
{--fornitore-term=NETHOME : Ragione sociale o frammento per trovare il fornitore di riferimento}
{--dry-run : Simula import senza scrivere}';
protected $description = 'Materializza la rubrica clienti del fornitore a partire dai record legacy TecnoRepair importati da TClienti.';
public function handle(): int
{
$admin = $this->resolveAdmin((string) $this->argument('amministratore'));
if (! $admin instanceof Amministratore) {
$this->error('Amministratore non trovato.');
return self::FAILURE;
}
$fornitore = $this->resolveFornitore($admin, (string) $this->option('fornitore-id'), (string) $this->option('fornitore-term'));
if (! $fornitore instanceof Fornitore) {
$this->error('Fornitore non trovato per questo amministratore.');
return self::FAILURE;
}
$dryRun = (bool) $this->option('dry-run');
$candidates = AssistenzaTecnorepairScheda::query()
->where('amministratore_id', (int) $admin->id)
->where('fornitore_id', (int) $fornitore->id)
->orderByDesc('date_received')
->orderByDesc('id')
->get();
$customers = $this->collectUniqueCustomers($candidates);
$stats = [
'legacy_rows' => $candidates->count(),
'clienti_distinti' => $customers->count(),
'rubrica_create' => 0,
'rubrica_match' => 0,
'rubrica_update' => 0,
'fornitore_rows' => 0,
];
$runner = function () use ($customers, $fornitore, $admin, &$stats): void {
foreach ($customers as $customer) {
$rubrica = $this->resolveOrCreateRubrica($admin, $customer, $stats);
FornitoreCliente::query()->updateOrCreate(
[
'fornitore_id' => (int) $fornitore->id,
'legacy_cliente_id' => $customer['legacy_cliente_id'],
],
[
'amministratore_id' => (int) $admin->id,
'rubrica_id' => $rubrica?->id,
'display_name' => $customer['display_name'],
'phone' => $customer['phone'],
'phone_alt' => $customer['phone_alt'],
'email' => $customer['email'],
'indirizzo' => $customer['indirizzo'],
'cap' => $customer['cap'],
'citta' => $customer['citta'],
'provincia' => $customer['provincia'],
'partita_iva' => $customer['partita_iva'],
'codice_fiscale' => $customer['codice_fiscale'],
'note' => $customer['note'],
'source' => 'tecnorepair_tclienti',
'imported_from_path' => $customer['imported_from_path'],
'imported_at' => now(),
'metadata' => $customer['metadata'],
]
);
$stats['fornitore_rows']++;
}
};
if ($dryRun) {
foreach ($customers as $customer) {
$this->resolveOrCreateRubrica($admin, $customer, $stats, true);
$stats['fornitore_rows']++;
}
} else {
DB::transaction($runner);
}
$this->info('Import rubrica clienti completato.');
$this->table(['Campo', 'Valore'], [
['Amministratore', (string) ($admin->denominazione_studio ?: trim((string) ($admin->nome ?? '') . ' ' . (string) ($admin->cognome ?? '')))],
['Fornitore', (string) ($fornitore->ragione_sociale ?: ('Fornitore #' . $fornitore->id))],
['Dry run', $dryRun ? 'si' : 'no'],
['Schede legacy lette', (string) $stats['legacy_rows']],
['Clienti distinti', (string) $stats['clienti_distinti']],
['Rubrica nuova', (string) $stats['rubrica_create']],
['Rubrica match', (string) $stats['rubrica_match']],
['Rubrica aggiornata', (string) $stats['rubrica_update']],
['Righe fornitore_clienti', (string) $stats['fornitore_rows']],
]);
return self::SUCCESS;
}
private function resolveAdmin(string $value): ?Amministratore
{
$value = trim($value);
if ($value === '') {
return null;
}
return Amministratore::query()
->when(is_numeric($value), fn($query) => $query->orWhere('id', (int) $value))
->orWhere('codice_amministratore', $value)
->orWhere('codice_univoco', $value)
->first();
}
private function resolveFornitore(Amministratore $admin, string $fornitoreIdOption, string $fornitoreTermOption): ?Fornitore
{
$fornitoreId = (int) $fornitoreIdOption;
if ($fornitoreId > 0) {
return Fornitore::query()
->where('amministratore_id', (int) $admin->id)
->whereKey($fornitoreId)
->first();
}
$term = trim($fornitoreTermOption);
if ($term === '') {
return null;
}
return Fornitore::query()
->where('amministratore_id', (int) $admin->id)
->where(function ($query) use ($term): void {
$query->where('ragione_sociale', 'like', '%' . $term . '%')
->orWhere('nome', 'like', '%' . $term . '%')
->orWhere('cognome', 'like', '%' . $term . '%');
})
->orderByDesc('is_tecnorepair_primary')
->orderBy('ragione_sociale')
->first();
}
/**
* @param Collection<int, AssistenzaTecnorepairScheda> $rows
* @return Collection<int, array<string, mixed>>
*/
private function collectUniqueCustomers(Collection $rows): Collection
{
$customers = [];
foreach ($rows as $scheda) {
$cliente = is_array($scheda->metadata['cliente'] ?? null) ? $scheda->metadata['cliente'] : [];
$legacyClienteId = $this->toInt($cliente['ID'] ?? $scheda->legacy_cliente_id);
$displayName = $this->cleanString($cliente['NomeCognome'] ?? $scheda->customer_name);
$phone = $this->cleanString($cliente['NumeroTelefono'] ?? $scheda->customer_phone);
$phoneAlt = $this->cleanString($cliente['TelFisso'] ?? $scheda->customer_phone_alt);
$email = $this->cleanString($cliente['Email'] ?? $scheda->customer_email);
$indirizzo = $this->cleanString($cliente['Indirizzo'] ?? null);
$cap = $this->cleanString($cliente['Cap'] ?? null);
$citta = $this->cleanString($cliente['Citta'] ?? null);
$provincia = $this->cleanString($cliente['Prov'] ?? null);
$partitaIva = $this->cleanString($cliente['PIVA'] ?? null);
$codiceFiscale = $this->cleanString($cliente['CodFis'] ?? null);
$note = $this->cleanString($cliente['Annotazioni'] ?? null);
$identity = $legacyClienteId !== null
? 'legacy-' . $legacyClienteId
: $this->buildIdentityKey($displayName, $email, $phone, $phoneAlt);
if ($identity === '' || ($displayName === null && $phone === null && $email === null)) {
continue;
}
if (! isset($customers[$identity])) {
$customers[$identity] = [
'legacy_cliente_id' => $legacyClienteId,
'display_name' => $displayName,
'phone' => $phone,
'phone_alt' => $phoneAlt,
'email' => $email,
'indirizzo' => $indirizzo,
'cap' => $cap,
'citta' => $citta,
'provincia' => $provincia,
'partita_iva' => $partitaIva,
'codice_fiscale' => $codiceFiscale,
'note' => $note,
'imported_from_path' => $this->cleanString($scheda->imported_from_path),
'metadata' => [
'cliente' => $cliente,
'ultima_scheda_id' => (int) $scheda->id,
'ultima_scheda_num' => (string) ($scheda->legacy_numero_scheda ?? ''),
],
];
continue;
}
foreach (['display_name', 'phone', 'phone_alt', 'email', 'indirizzo', 'cap', 'citta', 'provincia', 'partita_iva', 'codice_fiscale', 'note', 'imported_from_path'] as $field) {
if (($customers[$identity][$field] ?? null) === null && ${Str::camel($field);} !== null) {
$customers[$identity][$field] = ${Str::camel($field);}
}
}
}
return collect(array_values($customers));
}
/**
* @param array<string, mixed> $customer
* @param array<string, int> $stats
*/
private function resolveOrCreateRubrica(Amministratore $admin, array $customer, array &$stats, bool $dryRun = false): ?RubricaUniversale
{
$query = RubricaUniversale::query()->where('amministratore_id', (int) $admin->id);
$email = $this->cleanString($customer['email'] ?? null);
$phone = $this->normalizePhone($customer['phone'] ?? null);
$phoneAlt = $this->normalizePhone($customer['phone_alt'] ?? null);
$phoneForRubrica = $this->normalizePhoneField($customer['phone'] ?? null);
$phoneAltForRubrica = $this->normalizePhoneField($customer['phone_alt'] ?? null);
$cf = $this->cleanString($customer['codice_fiscale'] ?? null);
$piva = $this->cleanString($customer['partita_iva'] ?? null);
$name = $this->cleanString($customer['display_name'] ?? null);
$provincia = $this->normalizeProvince($customer['provincia'] ?? null);
$rubrica = null;
if ($cf !== null) {
$rubrica = (clone $query)->where('codice_fiscale', $cf)->first();
}
if (! $rubrica && $piva !== null) {
$rubrica = (clone $query)->where('partita_iva', $piva)->first();
}
if (! $rubrica && $email !== null) {
$rubrica = (clone $query)->where('email', $email)->first();
}
if (! $rubrica && $phone !== null) {
$rubrica = (clone $query)->where(function ($phoneQuery) use ($phone): void {
$phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phone])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phone])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phone]);
})->first();
}
if (! $rubrica && $phoneAlt !== null) {
$rubrica = (clone $query)->where(function ($phoneQuery) use ($phoneAlt): void {
$phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phoneAlt])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phoneAlt])
->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phoneAlt]);
})->first();
}
if (! $rubrica && $name !== null) {
$rubrica = (clone $query)->where(function ($nameQuery) use ($name): void {
$nameQuery->whereRaw("LOWER(TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))) = ?", [Str::lower($name)])
->orWhereRaw("LOWER(TRIM(COALESCE(ragione_sociale, ''))) = ?", [Str::lower($name)]);
})->first();
}
if ($rubrica instanceof RubricaUniversale) {
$stats['rubrica_match']++;
if (! $dryRun) {
$dirty = false;
foreach ([
'email' => $email,
'telefono_cellulare' => $phoneForRubrica,
'telefono_ufficio' => $phoneAltForRubrica,
'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null),
'cap' => $this->cleanString($customer['cap'] ?? null),
'citta' => $this->cleanString($customer['citta'] ?? null),
'provincia' => $provincia,
'partita_iva' => $piva,
'codice_fiscale' => $cf,
] as $field => $value) {
if ($value !== null && ! filled($rubrica->{$field})) {
$rubrica->{$field} = $value;
$dirty = true;
}
}
if ($dirty) {
$rubrica->save();
$stats['rubrica_update']++;
}
}
return $rubrica;
}
$stats['rubrica_create']++;
if ($dryRun) {
return null;
}
[$nome, $cognome] = $this->splitDisplayName($name);
return RubricaUniversale::query()->create([
'amministratore_id' => (int) $admin->id,
'nome' => $nome,
'cognome' => $cognome,
'tipo_contatto' => 'persona_fisica',
'codice_fiscale' => $cf,
'partita_iva' => $piva,
'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null),
'cap' => $this->cleanString($customer['cap'] ?? null),
'citta' => $this->cleanString($customer['citta'] ?? null),
'provincia' => $provincia,
'telefono_cellulare' => $phoneForRubrica,
'telefono_ufficio' => $phoneAltForRubrica,
'email' => $email,
'note' => $this->buildRubricaNote($customer),
'categoria' => 'cliente',
'stato' => 'attivo',
'data_inserimento' => now(),
'data_ultima_modifica' => now(),
'creato_da' => auth()->id(),
'modificato_da' => auth()->id(),
]);
}
private function splitDisplayName(?string $name): array
{
$name = trim((string) $name);
if ($name === '') {
return ['Cliente', 'TecnoRepair'];
}
$parts = preg_split('/\s+/', $name) ?: [];
if (count($parts) <= 1) {
return [$name, ''];
}
return [trim((string) array_shift($parts)), trim(implode(' ', $parts))];
}
private function buildRubricaNote(array $customer): string
{
$lines = array_filter([
'Import TecnoRepair TClienti per rubrica fornitore.',
isset($customer['legacy_cliente_id']) && $customer['legacy_cliente_id'] !== null ? 'Legacy cliente ID: ' . $customer['legacy_cliente_id'] : null,
$this->cleanString($customer['note'] ?? null),
]);
return implode("\n", $lines);
}
private function buildIdentityKey(?string $displayName, ?string $email, ?string $phone, ?string $phoneAlt): string
{
if ($email !== null) {
return 'email-' . Str::lower($email);
}
if ($phone !== null) {
return 'phone-' . preg_replace('/\D+/', '', $phone);
}
if ($phoneAlt !== null) {
return 'phone-' . preg_replace('/\D+/', '', $phoneAlt);
}
if ($displayName !== null) {
return 'name-' . Str::slug(Str::lower($displayName));
}
return '';
}
private function normalizePhone(mixed $value): ?string
{
$value = preg_replace('/\D+/', '', (string) $value) ?? '';
return $value !== '' ? $value : null;
}
private function normalizePhoneField(mixed $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
if (preg_match('/\+?[0-9][0-9\s().\/-]{4,}/', $value, $matches) !== 1) {
return null;
}
$normalized = preg_replace('/[^0-9+]/', '', (string) $matches[0]) ?? '';
$normalized = ltrim($normalized, '+') === '' ? '' : $normalized;
return $normalized !== '' ? Str::limit($normalized, 30, '') : null;
}
private function normalizeProvince(mixed $value): ?string
{
$value = Str::upper(trim((string) $value));
if ($value === '') {
return null;
}
$value = preg_replace('/[^A-Z]/', '', $value) ?? '';
if ($value === '') {
return null;
}
return Str::limit($value, 2, '');
}
private function cleanString(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function toInt(mixed $value): ?int
{
if ($value === null || $value === '' || ! is_numeric($value)) {
return null;
}
return (int) $value;
}
}