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

792 lines
30 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use App\Models\AliquotaIva;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\RegimeFiscale;
use App\Models\RubricaUniversale;
use App\Models\TipologiaCassa;
use App\Models\Titolo;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class GesconImportFornitoriLegacyMdb extends Command
{
protected $signature = 'gescon:import-fornitori-legacy
{amministratore_id : ID amministratore a cui associare i fornitori}
{--mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb : Percorso file Fornitori.mdb}
{--table=Fornitori : Nome tabella Access}
{--dry-run : Non scrive su DB, mostra solo riepilogo}
{--limit=0 : Limita righe elaborate (0 = nessun limite)}';
protected $description = 'Importa i fornitori legacy da GESCON (Fornitori.mdb) in Rubrica + Fornitori, mappando anche regime fiscale e contatti (tel/email/pec).';
/** @var array<string, array<int, string>> */
private array $columnCache = [];
public function handle(): int
{
$amministratoreId = (int) $this->argument('amministratore_id');
$mdb = (string) $this->option('mdb');
$table = (string) $this->option('table');
$dry = (bool) $this->option('dry-run');
$limit = max(0, (int) $this->option('limit'));
if ($amministratoreId <= 0) {
$this->error('amministratore_id non valido');
return self::FAILURE;
}
if ($mdb === '' || ! is_file($mdb)) {
$this->error("File MDB non trovato: {$mdb}");
return self::FAILURE;
}
$bin = trim((string) @shell_exec('command -v mdb-export'));
if ($bin === '') {
$this->error('mdb-export non trovato (installa mdbtools)');
return self::FAILURE;
}
// NOTE: alcuni archivi GESCON contengono doppie-virgolette non escape-ate nei campi.
// Se usiamo enclosure '"', l'output CSV diventa invalido e fgetcsv() può "mangiare" righe.
// Usiamo quindi un enclosure alternativo (^) per rendere robusto il parsing.
$cmd = sprintf(
'%s -D %s -q %s %s %s',
escapeshellarg($bin),
escapeshellarg('%Y-%m-%d %H:%M:%S'),
escapeshellarg('^'),
escapeshellarg($mdb),
escapeshellarg($table),
);
$csv = @shell_exec($cmd . ' 2>/dev/null');
if (! is_string($csv) || trim($csv) === '') {
$this->error('Nessun dato esportato (tabella inesistente o file non leggibile)');
return self::FAILURE;
}
[$rows, $headers] = $this->parseCsv($csv);
if (empty($rows)) {
$this->warn('Tabella vuota: nessun fornitore da importare');
return self::SUCCESS;
}
if ($limit > 0) {
$rows = array_slice($rows, 0, $limit);
}
$this->info('Colonne rilevate: ' . implode(', ', array_slice($headers, 0, 20)) . (count($headers) > 20 ? ' ...' : ''));
$stats = [
'rows' => 0,
'created_rubrica' => 0,
'updated_rubrica' => 0,
'created_fornitori' => 0,
'updated_fornitori' => 0,
'created_dipendenti' => 0,
'updated_dipendenti' => 0,
'skipped' => 0,
];
$runner = function () use (&$stats, $rows, $amministratoreId, $dry): void {
foreach ($rows as $row) {
$stats['rows']++;
$codForn = $this->toInt($row['cod_forn'] ?? null);
$legacyId = $codForn;
$legacyIdFornitore = $this->toInt($row['id_fornitore'] ?? null);
$ragione = $this->clean($row['denominazione'] ?? null);
// NB: `appoggio` in GESCON è un campo di servizio e spesso contiene blob di contatti.
// Non deve mai diventare la ragione sociale.
$appoggio = $this->clean($row['appoggio'] ?? null);
// Alcuni archivi hanno il campo denominazione sporco (es. "Tel.: ... Cell. ...").
// In quel caso NON deve finire come ragione sociale.
$contactBlob = null;
if (is_string($ragione) && $ragione !== '' && $this->looksLikeContactBlob($ragione)) {
$contactBlob = $ragione;
$ragione = null;
}
$cognome = $this->clean($this->pick($row, ['cognome', 'cognome_fornitore']));
$nome = $this->clean($this->pick($row, ['nome', 'nome_fornitore']));
if ($ragione === null) {
$full = trim(($cognome ?? '') . ' ' . ($nome ?? ''));
$ragione = $full !== '' ? $full : null;
}
$legacyDtNas = $this->normalizeLegacyDate($this->pick($row, ['dt_nas', 'data_nascita', 'dt_nascita']));
$legacyLuoNas = $this->clean($this->pick($row, ['luo_nas', 'luogo_nascita']));
$legacyPrNas = $this->clean($this->pick($row, ['pr_nas', 'prov_nascita', 'provincia_nascita']));
$legacySesso = $this->normalizeSesso($this->pick($row, ['sesso', 'sex']));
$legacyTitoloRaw = $this->clean($this->pick($row, ['titolo', 'titolo_fornitore', 'tit']));
$legacyEmail = $this->clean($this->pick($row, ['indir_email', 'email', 'mail']));
$legacyIntestazioneCcEsatta = $this->clean($this->pick($row, [
'intestaz_cc_esatta',
'intestaz_cc_esatto',
'intestazione_cc_esatta',
'intestatario_cc',
'intestatario_conto',
]));
$note = $this->clean($row['note'] ?? null);
if ($contactBlob) {
$note = $note ? ($note . "\n" . $contactBlob) : $contactBlob;
}
if ($appoggio) {
$note = $note ? ($note . "\n" . 'Appoggio: ' . $appoggio) : ('Appoggio: ' . $appoggio);
}
$legacyIdentityBits = array_values(array_filter([
$legacyTitoloRaw ? ('Titolo: ' . $legacyTitoloRaw) : null,
$legacySesso ? ('Sesso: ' . $legacySesso) : null,
$legacyDtNas ? ('Data nascita: ' . $legacyDtNas) : null,
$legacyLuoNas ? ('Luogo nascita: ' . $legacyLuoNas) : null,
$legacyPrNas ? ('Provincia nascita: ' . $legacyPrNas) : null,
]));
$piva = $this->clean($row['p_iva'] ?? null);
$cf = $this->clean($row['cod_fisc'] ?? null);
if ($legacyId === null && $codForn === null && $ragione === null && $piva === null && $cf === null) {
$stats['skipped']++;
continue;
}
$rubricaPayload = [
'amministratore_id' => $amministratoreId,
'ragione_sociale' => $ragione,
'nome' => $nome,
'cognome' => $cognome,
'partita_iva' => $piva,
'codice_fiscale' => $cf,
'sesso' => $legacySesso,
'data_nascita' => $legacyDtNas,
'luogo_nascita' => $legacyLuoNas,
'provincia_nascita' => $legacyPrNas,
'indirizzo' => $this->clean($row['indirizzo'] ?? null),
'cap' => $this->clean($row['cap'] ?? null),
'citta' => $this->clean($row['citta'] ?? null),
'provincia' => $this->clean($row['pr'] ?? null),
'telefono_ufficio' => $this->clean($row['telef_1'] ?? $row['telef_2'] ?? null),
'telefono_cellulare' => $this->clean($row['cellulare'] ?? null),
'fax' => $this->clean($row['fax'] ?? null),
'email' => $legacyEmail,
'pec' => $this->clean($row['pec_fornitore'] ?? null),
'note' => $note,
'categoria' => 'fornitore',
'stato' => 'attivo',
'tipo_contatto' => ($piva || ($ragione && ! $nome && ! $cognome)) ? 'persona_giuridica' : 'persona_fisica',
];
$titoloId = $this->resolveTitoloId($legacyTitoloRaw);
if ($titoloId !== null) {
$rubricaPayload['titolo_id'] = $titoloId;
}
$rubricaPayload = $this->filterPayloadForTable('rubrica_universale', $rubricaPayload);
$rubricaResult = $this->upsertRubrica($rubricaPayload, $legacyId);
$rubrica = $rubricaResult['rubrica'] ?? null;
if (! $rubrica) {
$stats['skipped']++;
continue;
}
// Se il codice_univoco viene assegnato via trigger MySQL, il model Eloquent potrebbe non averlo valorizzato.
// Lo rileggo dal DB per usarlo in dedup/merge e per evitare violazioni di unique sui fornitori.
$rubricaCode = $rubrica->codice_univoco;
if ((! is_string($rubricaCode) || $rubricaCode === '') && (int) $rubrica->id > 0) {
$rubricaCode = RubricaUniversale::query()->whereKey((int) $rubrica->id)->value('codice_univoco');
if (is_string($rubricaCode) && $rubricaCode !== '') {
$rubrica->codice_univoco = $rubricaCode;
}
}
if (($rubricaResult['created'] ?? false) === true) {
$stats['created_rubrica']++;
} elseif (($rubricaResult['updated'] ?? false) === true) {
$stats['updated_rubrica']++;
}
$regimeCode = $this->clean($row['regime_fiscale'] ?? null);
$regimeId = $this->resolveRegimeFiscaleId($regimeCode);
$tipoCassaCode = $this->clean($row['tipo_cassa'] ?? null);
$tipoCassaId = $this->resolveTipologiaCassaId($tipoCassaCode);
$aliqIvaAbituale = $this->clean($row['aliq_iva_abituale'] ?? $row['aliq_iva'] ?? null);
$aliquotaIvaId = $this->resolveAliquotaIvaId($aliqIvaAbituale);
$fornitorePayload = [
'amministratore_id' => $amministratoreId,
'cod_forn' => $codForn,
'rubrica_id' => $rubrica->id,
'codice_univoco' => (is_string($rubricaCode) && $rubricaCode !== '') ? $rubricaCode : null,
'ragione_sociale' => $ragione ?? $rubrica->ragione_sociale,
'nome' => $nome ?? $rubrica->nome,
'cognome' => $cognome ?? $rubrica->cognome,
'partita_iva' => $piva ?? $rubrica->partita_iva,
'codice_fiscale' => $cf ?? $rubrica->codice_fiscale,
'indirizzo' => $rubricaPayload['indirizzo'] ?? $rubrica->indirizzo,
'cap' => $rubricaPayload['cap'] ?? $rubrica->cap,
'citta' => $rubricaPayload['citta'] ?? $rubrica->citta,
'provincia' => $rubricaPayload['provincia'] ?? $rubrica->provincia,
'email' => $rubricaPayload['email'] ?? $rubrica->email,
'pec' => $rubricaPayload['pec'] ?? $rubrica->pec,
'iban' => $this->clean($row['cod_iban'] ?? null),
'intestazione_cc_esatta' => $legacyIntestazioneCcEsatta,
'telefono' => $rubricaPayload['telefono_ufficio'] ?? $rubrica->telefono_ufficio,
'cellulare' => $rubricaPayload['telefono_cellulare'] ?? $rubrica->telefono_cellulare,
'note' => $rubricaPayload['note'] ?? $rubrica->note,
'regime_fiscale_id' => $regimeId,
'tipologia_cassa_id' => $tipoCassaId,
'aliquota_iva_id' => $aliquotaIvaId,
'old_id' => $legacyId,
];
$fornitorePayload = $this->filterPayloadForTable('fornitori', $fornitorePayload);
$existing = null;
if ($legacyId !== null) {
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('old_id', $legacyId)
->first();
}
if (! $existing && $codForn !== null && Schema::hasColumn('fornitori', 'cod_forn')) {
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('cod_forn', $codForn)
->first();
}
if (! $existing && ! empty($fornitorePayload['rubrica_id'])) {
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('rubrica_id', $fornitorePayload['rubrica_id'])
->first();
}
if (! $existing && is_string($rubricaCode) && $rubricaCode !== '') {
// Evita violazioni di unique(amministratore_id, codice_univoco)
// Se esiste già un fornitore con lo stesso codice_univoco della rubrica, aggiorniamo quello.
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('codice_univoco', $rubricaCode)
->first();
}
if (! $existing && $piva) {
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('partita_iva', $piva)
->first();
}
if (! $existing && $cf) {
$existing = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('codice_fiscale', $cf)
->first();
}
$fornitorePayload = array_filter(
$fornitorePayload,
fn($v) => ! is_string($v) ? $v !== null : trim($v) !== ''
);
$fornitoreModel = null;
if ($existing) {
if (! empty($fornitorePayload['codice_univoco'])) {
$dup = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('codice_univoco', $fornitorePayload['codice_univoco'])
->where('id', '!=', $existing->id)
->exists();
if ($dup) {
unset($fornitorePayload['codice_univoco']);
}
}
$existing->fill($fornitorePayload);
if ($existing->isDirty()) {
if (! $dry) {
$existing->save();
}
$stats['updated_fornitori']++;
}
$fornitoreModel = $existing;
} else {
if (! empty($fornitorePayload['codice_univoco'])) {
$dup = Fornitore::query()
->where('amministratore_id', $amministratoreId)
->where('codice_univoco', $fornitorePayload['codice_univoco'])
->exists();
if ($dup) {
unset($fornitorePayload['codice_univoco']);
}
}
if ($dry) {
$fornitoreModel = new Fornitore($fornitorePayload);
} else {
$fornitoreModel = Fornitore::create($fornitorePayload);
}
$stats['created_fornitori']++;
}
if (! $fornitoreModel instanceof Fornitore) {
continue;
}
$dipResult = $this->upsertDipendenteCollegato(
$fornitoreModel,
$legacyEmail,
$nome,
$cognome,
$rubricaPayload['telefono_cellulare'] ?? null,
$legacyTitoloRaw,
$legacySesso,
$legacyDtNas,
$legacyLuoNas,
$legacyPrNas,
$dry,
);
if ($dipResult === 'created') {
$stats['created_dipendenti']++;
} elseif ($dipResult === 'updated') {
$stats['updated_dipendenti']++;
}
}
};
if ($dry) {
$runner();
} else {
DB::transaction($runner);
}
$this->info('✅ Import completato' . ($dry ? ' (dry-run)' : ''));
$this->line(json_encode($stats, JSON_UNESCAPED_UNICODE));
return self::SUCCESS;
}
/** @return array{0: array<int,array<string,mixed>>, 1: array<int,string>} */
private function parseCsv(string $csv): array
{
$fh = fopen('php://temp', 'r+');
fwrite($fh, $csv);
rewind($fh);
$headers = fgetcsv($fh, 0, ',', '^');
if (! $headers) {
return [[], []];
}
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
$rows = [];
while (($cols = fgetcsv($fh, 0, ',', '^')) !== false) {
if (empty($cols)) {
continue;
}
if (count($cols) < count($headers)) {
$cols = array_pad($cols, count($headers), null);
}
if (count($cols) > count($headers)) {
$cols = array_slice($cols, 0, count($headers));
}
$assoc = @array_combine($headers, $cols);
if (! $assoc) {
continue;
}
$rows[] = $assoc;
}
return [$rows, $headers];
}
/** @param array<string, mixed> $payload
* @return array{rubrica: RubricaUniversale|null, created: bool, updated: bool}
*/
private function upsertRubrica(array $payload, ?int $legacyId): array
{
$amministratoreId = $this->toInt($payload['amministratore_id'] ?? null);
// Rubrica multi-tenant "soft": match per amministratore o globale (NULL).
// Se amministratore_id non è presente nello schema, lo scope non viene applicato.
$query = RubricaUniversale::query();
if (Schema::hasColumn('rubrica_universale', 'amministratore_id') && $amministratoreId !== null) {
$query->where(function ($q) use ($amministratoreId) {
$q->where('amministratore_id', $amministratoreId)
->orWhereNull('amministratore_id');
});
}
$piva = $payload['partita_iva'] ?? null;
$cf = $payload['codice_fiscale'] ?? null;
$ragione = $payload['ragione_sociale'] ?? null;
if ($piva) {
$existing = (clone $query)->where('partita_iva', $piva)->first();
} elseif ($cf) {
$existing = (clone $query)->where('codice_fiscale', $cf)->first();
} elseif ($ragione) {
$existing = (clone $query)->whereRaw('LOWER(ragione_sociale) = ?', [mb_strtolower($ragione)])
->first();
} else {
$existing = null;
}
$payload = array_filter(
$payload,
fn($v) => ! is_string($v) ? $v !== null : trim($v) !== ''
);
if ($this->option('dry-run')) {
if ($existing) {
$existing->fill($payload);
return [
'rubrica' => $existing,
'created' => false,
'updated' => $existing->isDirty(),
];
}
return [
'rubrica' => new RubricaUniversale($payload),
'created' => true,
'updated' => false,
];
}
if ($existing) {
$existing->fill($payload);
$dirty = $existing->isDirty();
if ($dirty) {
$existing->save();
}
return [
'rubrica' => $existing,
'created' => false,
'updated' => $dirty,
];
}
return [
'rubrica' => RubricaUniversale::create($payload),
'created' => true,
'updated' => false,
];
}
/** @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function filterPayloadForTable(string $table, array $payload): array
{
if (! Schema::hasTable($table)) {
return $payload;
}
if (! isset($this->columnCache[$table])) {
$this->columnCache[$table] = Schema::getColumnListing($table);
}
$allowed = array_flip($this->columnCache[$table]);
$filtered = array_intersect_key($payload, $allowed);
return array_filter(
$filtered,
fn($v) => ! is_string($v) ? $v !== null : trim($v) !== ''
);
}
private function resolveRegimeFiscaleId(?string $code): ?int
{
$code = $this->clean($code);
if ($code === null) {
return null;
}
$key = strtolower($code);
$regime = RegimeFiscale::query()
->whereRaw('LOWER(codice) = ?', [$key])
->orWhereRaw('LOWER(descrizione) = ?', [$key])
->first();
return $regime?->id;
}
private function resolveTipologiaCassaId(?string $code): ?int
{
$code = $this->clean($code);
if ($code === null) {
return null;
}
$key = strtolower($code);
$record = TipologiaCassa::query()
->whereRaw('LOWER(codice) = ?', [$key])
->orWhereRaw('LOWER(descrizione) = ?', [$key])
->first();
return $record?->id;
}
private function resolveAliquotaIvaId(?string $valueOrCode): ?int
{
$valueOrCode = $this->clean($valueOrCode);
if ($valueOrCode === null) {
return null;
}
// Supporta sia codici (es. IVA22) sia valori numerici (es. 22)
$key = strtolower($valueOrCode);
$byCode = AliquotaIva::query()
->whereRaw('LOWER(codice) = ?', [$key])
->first();
if ($byCode) {
return $byCode->id;
}
$numeric = str_replace(',', '.', $valueOrCode);
if (is_numeric($numeric)) {
$val = (float) $numeric;
$byValue = AliquotaIva::query()
->where('valore', $val)
->first();
return $byValue?->id;
}
return null;
}
private function clean($v): ?string
{
if ($v === null) {
return null;
}
$s = (string) $v;
// Normalizza newline/controlli e compatta spazi, mantenendo una stringa leggibile.
$s = str_replace(["\r\n", "\r"], "\n", $s);
$s = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $s) ?? $s;
$s = preg_replace('/\s+/u', ' ', $s) ?? $s;
$s = trim($s);
return $s === '' ? null : $s;
}
private function looksLikeContactBlob(string $s): bool
{
$s = trim($s);
if ($s === '') {
return false;
}
// Esempi tipici legacy: "Tel.: ...", "Cell. ...", "Fax: ..."
if (preg_match('/^(tel\.?|cell\.?|fax\.?|telefono|mobile)\b\s*[:\.]/i', $s)) {
return true;
}
// Stringhe composte solo da numeri/segni (telefono senza label)
if (preg_match('/^[\d\s+().\-\/]+$/', $s)) {
return true;
}
return false;
}
private function toInt($v): ?int
{
if ($v === null) {
return null;
}
$s = trim((string) $v);
if ($s === '' || ! is_numeric($s)) {
return null;
}
return (int) $s;
}
/**
* @param array<string, mixed> $row
*/
private function pick(array $row, array $keys): mixed
{
foreach ($keys as $key) {
if (array_key_exists($key, $row)) {
return $row[$key];
}
$lower = strtolower((string) $key);
if (array_key_exists($lower, $row)) {
return $row[$lower];
}
}
return null;
}
private function normalizeLegacyDate(mixed $value): ?string
{
$raw = $this->clean($value);
if ($raw === null) {
return null;
}
$raw = trim($raw);
foreach (['d/m/Y', 'd-m-Y', 'Y-m-d', 'm/d/Y', 'd.m.Y', 'd/m/y', 'd-m-y', 'm/d/y', 'd.m.y'] as $format) {
$dt = \DateTime::createFromFormat($format, $raw);
if ($dt instanceof \DateTime) {
return $this->normalizeBirthDateYear($dt);
}
}
$ts = strtotime($raw);
if ($ts !== false) {
return $this->normalizeBirthDateYear((new \DateTime())->setTimestamp($ts));
}
return null;
}
private function normalizeBirthDateYear(\DateTime $date): ?string
{
$year = (int) $date->format('Y');
$currentYear = (int) now()->format('Y');
if ($year > $currentYear + 1) {
$date->modify('-100 years');
$year = (int) $date->format('Y');
}
if ($year < 1900 || $year > $currentYear + 1) {
return null;
}
return $date->format('Y-m-d');
}
private function normalizeSesso(mixed $value): ?string
{
$raw = strtoupper((string) $this->clean($value));
if ($raw === 'M' || $raw === 'F') {
return $raw;
}
if (str_starts_with($raw, 'MAS')) {
return 'M';
}
if (str_starts_with($raw, 'FEM')) {
return 'F';
}
return null;
}
private function resolveTitoloId(?string $raw): ?int
{
$raw = $this->clean($raw);
if ($raw === null || ! Schema::hasTable('titoli')) {
return null;
}
$key = mb_strtolower($raw);
$title = Titolo::query()
->whereRaw('LOWER(sigla) = ?', [$key])
->orWhereRaw('LOWER(nome) = ?', [$key])
->first();
return $title?->id;
}
private function upsertDipendenteCollegato(
Fornitore $fornitore,
?string $email,
?string $nome,
?string $cognome,
?string $telefono,
?string $titolo,
?string $sesso,
?string $dataNascita,
?string $luogoNascita,
?string $provinciaNascita,
bool $dry,
): ?string {
$email = $this->clean($email);
$nome = $this->clean($nome);
$cognome = $this->clean($cognome);
$telefono = $this->clean($telefono);
if ($email === null && $nome === null && $cognome === null) {
return null;
}
$displayName = trim((string) (($nome ?? '') . ' ' . ($cognome ?? '')));
if ($displayName === '' && $email !== null) {
$displayName = strtok($email, '@') ?: 'Referente';
$displayName = str_replace(['.', '_', '-'], ' ', $displayName);
}
$resolvedNome = $nome ?: trim((string) strtok($displayName, ' '));
$resolvedCognome = $cognome;
if ($resolvedCognome === null) {
$parts = preg_split('/\s+/', trim($displayName)) ?: [];
if (count($parts) > 1) {
array_shift($parts);
$resolvedCognome = trim((string) implode(' ', $parts)) ?: null;
}
}
$meta = array_values(array_filter([
$titolo ? ('Titolo: ' . $titolo) : null,
$sesso ? ('Sesso: ' . $sesso) : null,
$dataNascita ? ('Data nascita: ' . $dataNascita) : null,
$luogoNascita ? ('Luogo nascita: ' . $luogoNascita) : null,
$provinciaNascita ? ('Provincia nascita: ' . $provinciaNascita) : null,
]));
$notes = $meta !== [] ? ('Import legacy GESCON. ' . implode(' | ', $meta)) : 'Import legacy GESCON.';
$query = FornitoreDipendente::query()->where('fornitore_id', (int) $fornitore->id);
if ($email !== null) {
$query->whereRaw('LOWER(email) = ?', [mb_strtolower($email)]);
} elseif ($resolvedNome !== null) {
$query->whereRaw('LOWER(nome) = ?', [mb_strtolower($resolvedNome)]);
if ($resolvedCognome !== null) {
$query->whereRaw('LOWER(cognome) = ?', [mb_strtolower($resolvedCognome)]);
}
}
$existing = $query->first();
$payload = [
'nome' => $resolvedNome ?: 'Referente',
'cognome' => $resolvedCognome,
'email' => $email,
'telefono' => $telefono,
'attivo' => true,
'note' => $notes,
];
if ($existing) {
$existing->fill($payload);
if (! $existing->isDirty()) {
return null;
}
if (! $dry) {
$existing->save();
}
return 'updated';
}
if (! $dry) {
FornitoreDipendente::query()->create(array_merge($payload, [
'fornitore_id' => (int) $fornitore->id,
]));
}
return 'created';
}
}