441 lines
18 KiB
PHP
441 lines
18 KiB
PHP
<?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;
|
|
}
|
|
} |