netgescon-day0/app/Services/Tecnorepair/TecnoRepairArchiveService.php

508 lines
21 KiB
PHP

<?php
namespace App\Services\Tecnorepair;
use App\Models\Amministratore;
use App\Models\AssistenzaTecnorepairAllegato;
use App\Models\AssistenzaTecnorepairScheda;
use App\Models\Fornitore;
use App\Models\Product;
use App\Models\ProductIdentifier;
use App\Models\ProductOffer;
use App\Models\ProductSerial;
use App\Services\Catalog\FornitoreProductCatalogService;
use App\Services\Catalog\ProductOfferService;
use App\Support\TecnoRepairMdbReader;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;
class TecnoRepairArchiveService
{
public const DEFAULT_WINDOWS_UNC_PATH = '\\\\192.168.0.36\\CServerGO\\LunaSoftware_TecnoRepair\\Archivi\\TecnoRepairDB.mdb';
public const DEFAULT_LOCAL_PATH = '/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb';
public function __construct(
private readonly TecnoRepairMdbReader $reader,
private readonly FornitoreProductCatalogService $catalogService,
private readonly ProductOfferService $productOfferService,
) {
}
/**
* Resolve the readable local path for TecnoRepairDB.mdb, handling Windows UNC paths and local replicas.
*/
public function resolveMdbPath(?string $candidatePath = null, ?Fornitore $fornitore = null): string
{
$path = trim((string) $candidatePath);
if ($path === '' && $fornitore instanceof Fornitore) {
$config = (array) ($fornitore->operational_config ?? []);
$path = trim((string) data_get($config, 'tecnorepair.mdb_path', ''));
if ($path === '') {
$path = trim((string) data_get($config, 'tecnorepair.local_fallback_path', ''));
}
}
if ($path === '') {
$path = self::DEFAULT_WINDOWS_UNC_PATH;
}
// If path is a local existing file, return realpath
if (file_exists($path) && is_file($path)) {
return realpath($path) ?: $path;
}
// Candidates for fallback
$fallbackCandidates = [
self::DEFAULT_LOCAL_PATH,
'/home/michele/MIki/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb',
'/mnt/gescon-archives/TecnoRepairDB.mdb',
storage_path('app/private/tecnorepair/TecnoRepairDB.mdb'),
];
if ($fornitore instanceof Fornitore) {
$config = (array) ($fornitore->operational_config ?? []);
$customFallback = trim((string) data_get($config, 'tecnorepair.local_fallback_path', ''));
if ($customFallback !== '') {
array_unshift($fallbackCandidates, $customFallback);
}
}
foreach ($fallbackCandidates as $cand) {
if (file_exists($cand) && is_file($cand) && is_readable($cand)) {
return realpath($cand) ?: $cand;
}
}
throw new RuntimeException("Archivio TecnoRepair non trovato sul percorso specificato ($path) e nessuna replica locale trovata.");
}
/**
* Import entire TecnoRepair archive (Schede, Clienti, Allegati, Ricambi/Prodotti).
*
* @return array<string, mixed>
*/
public function importArchive(
int $amministratoreId,
int $fornitoreId,
?string $mdbPath = null,
int $limit = 0,
bool $dryRun = false
): array {
$fornitore = Fornitore::query()->findOrFail($fornitoreId);
$resolvedPath = $this->resolveMdbPath($mdbPath, $fornitore);
$tables = $this->reader->listTables($resolvedPath);
foreach (['TClienti', 'TApparecchi', 'TAllegati'] as $requiredTable) {
if (! in_array($requiredTable, $tables, true)) {
throw new RuntimeException("Tabella richiesta mancante nel file MDB: {$requiredTable}");
}
}
$clientiRows = $this->reader->exportTable($resolvedPath, 'TClienti');
$schedeRows = $this->reader->exportTable($resolvedPath, 'TApparecchi');
$allegatiRows = $this->reader->exportTable($resolvedPath, 'TAllegati');
$ricambiRows = in_array('TRicambiAnag', $tables, true)
? $this->reader->exportTable($resolvedPath, 'TRicambiAnag')
: [];
$ricambiSchedaRows = in_array('TRicambiScheda', $tables, true)
? $this->reader->exportTable($resolvedPath, 'TRicambiScheda')
: [];
$schedeRows = array_reverse($schedeRows);
if ($limit > 0) {
$schedeRows = array_slice($schedeRows, 0, $limit);
}
$clienti = [];
foreach ($clientiRows as $clienteRow) {
$cId = $this->toInt($clienteRow['ID'] ?? null);
if ($cId !== null) {
$clienti[$cId] = $clienteRow;
}
}
$allegatiByScheda = [];
foreach ($allegatiRows as $allegatoRow) {
$sId = $this->toInt($allegatoRow['ID_Scheda'] ?? null);
if ($sId !== null) {
$allegatiByScheda[$sId][] = $allegatoRow;
}
}
$ricambiByScheda = [];
foreach ($ricambiSchedaRows as $rs) {
$sId = $this->toInt($rs['ID_Scheda'] ?? null);
if ($sId !== null) {
$ricambiByScheda[$sId][] = $rs;
}
}
$stats = [
'mdb_path' => $resolvedPath,
'schede_lette' => count($schedeRows),
'schede_create' => 0,
'schede_aggiornate' => 0,
'schede_saltate' => 0,
'allegati_importati' => 0,
'seriali_allineati' => 0,
'ricambi_catalogati' => 0,
'dry_run' => $dryRun,
];
// 1. Process Ricambi / Spare parts as Products
if (! empty($ricambiRows)) {
foreach ($ricambiRows as $ricambio) {
$codice = trim((string) ($ricambio['Codice'] ?? ''));
$descrizione = trim((string) ($ricambio['Descrizione'] ?? ''));
if ($codice === '' && $descrizione === '') {
continue;
}
if ($dryRun) {
$stats['ricambi_catalogati']++;
continue;
}
$barcode = trim((string) ($ricambio['CodBarre'] ?? ''));
$prezzoAcq = (float) str_replace(',', '.', (string) ($ricambio['PrezzoAcq'] ?? 0));
$prezzoUltAcq = (float) str_replace(',', '.', (string) ($ricambio['PrezzoUltAcq'] ?? 0));
$costo = $prezzoAcq > 0 ? $prezzoAcq : ($prezzoUltAcq > 0 ? $prezzoUltAcq : null);
$przList1 = (float) str_replace(',', '.', (string) ($ricambio['PrzList1'] ?? 0));
$name = $descrizione !== '' ? $descrizione : ('Ricambio ' . $codice);
$res = $this->catalogService->resolveOrCreateProduct($fornitore, [
'name' => $name,
'internal_code' => $codice ?: ('RIC-' . ($ricambio['ID'] ?? uniqid())),
'type' => 'spare_part',
'brand' => 'TecnoRepair',
'description' => $descrizione,
'track_serials' => true,
'meta' => [
'source' => 'tecnorepair_ricambi',
'raw_id' => $ricambio['ID'] ?? null,
'pos_magazzino' => $ricambio['PosizioneMagazzino'] ?? null,
'prz_list1' => $przList1,
],
'identifiers' => array_filter([
$codice !== '' ? [
'fornitore_id' => $fornitore->id,
'code_type' => 'vendor_sku',
'code_role' => 'primary',
'code_value' => $codice,
'source' => 'tecnorepair_anag',
] : null,
$barcode !== '' ? [
'fornitore_id' => $fornitore->id,
'code_type' => 'barcode',
'code_role' => 'barcode',
'code_value' => $barcode,
'source' => 'tecnorepair_anag',
] : null,
]),
]);
$product = $res['product'];
if ($costo !== null && $costo > 0) {
$this->productOfferService->syncInternalSupplierOffer($product, $fornitore, [
'price_amount' => $costo,
'currency' => 'EUR',
'availability' => 'in_stock',
'meta' => [
'prz_list1' => $przList1,
'source_type' => 'tecnorepair_ricambi',
],
]);
}
$stats['ricambi_catalogati']++;
}
}
// 2. Process Schede / Repair cards
$runner = function () use (
$amministratoreId,
$fornitore,
$resolvedPath,
$schedeRows,
$clienti,
$allegatiByScheda,
$ricambiByScheda,
$dryRun,
&$stats
): void {
foreach ($schedeRows as $row) {
$legacyId = $this->toInt($row['ID'] ?? null);
if ($legacyId === null) {
$stats['schede_saltate']++;
continue;
}
$legacyClienteId = $this->toInt($row['ID_Cliente'] ?? null);
$cliente = $legacyClienteId !== null ? ($clienti[$legacyClienteId] ?? []) : [];
$statusLabel = $this->clean($row['StatoRiparazione'] ?? null) ?: $this->clean($row['ID_StatoRip'] ?? null);
$payload = [
'amministratore_id' => $amministratoreId,
'fornitore_id' => $fornitore->id,
'legacy_id' => $legacyId,
'legacy_cliente_id' => $legacyClienteId,
'legacy_centro_ass_id' => $this->toInt($row['ID_CentroAss'] ?? null),
'legacy_committente_codice' => $this->clean($row['Cod_Committente'] ?? null),
'legacy_numero_scheda' => $this->clean($row['NumeroScheda'] ?? null),
'customer_name' => $this->clean($cliente['NomeCognome'] ?? null),
'customer_phone' => $this->clean($cliente['NumeroTelefono'] ?? null),
'customer_phone_alt' => $this->clean($cliente['TelFisso'] ?? null),
'customer_email' => $this->clean($cliente['Email'] ?? null),
'product_model' => $this->clean($row['Modello'] ?? null),
'product_code' => $this->clean($row['CodiceProdotto'] ?? null),
'serial_number' => $this->clean($row['SerialNumber'] ?? null),
'serial_number_2' => $this->clean($row['SerialNumber2'] ?? null),
'status_code' => $this->clean($row['ID_StatoRip'] ?? null),
'status_label' => $statusLabel,
'status_bucket' => AssistenzaTecnorepairScheda::normalizeStatusBucket($statusLabel),
'defect_reported' => $this->clean($row['DifettoSegnalato'] ?? null),
'repair_description' => $this->clean($row['DescrizioneRiparazione'] ?? null),
'communications' => $this->clean($row['Comunicazioni'] ?? null),
'operator_name' => $this->clean($row['NomeOperatore'] ?? null),
'technician_name' => $this->clean($row['NomeTecnicoRiparatore'] ?? null),
'date_received' => $this->normalizeDate($row['DataIngresso'] ?? null),
'ordered_at' => $this->normalizeDate($row['DataOrdine'] ?? null),
'order_number' => $this->clean($row['NumOrdine'] ?? null),
'rma_code' => $this->clean($row['CodiceRMA'] ?? null),
'pin_code' => $this->clean($row['CodicePIN'] ?? null),
'unlock_code' => $this->clean($row['CodiceSblocco'] ?? null),
'legacy_attachment_path' => $this->clean($row['FileAllegato1'] ?? null),
'imported_from_path' => $resolvedPath,
'imported_at' => now(),
'metadata' => [
'cliente' => $cliente,
'raw' => $row,
'ricambi_scheda' => $ricambiByScheda[$legacyId] ?? [],
],
];
$existing = AssistenzaTecnorepairScheda::query()
->where('amministratore_id', $amministratoreId)
->where('legacy_id', $legacyId)
->first();
if ($dryRun) {
if ($existing) {
$stats['schede_aggiornate']++;
} else {
$stats['schede_create']++;
}
continue;
}
if ($existing instanceof AssistenzaTecnorepairScheda) {
$existing->fill($payload);
$existing->save();
$scheda = $existing;
$stats['schede_aggiornate']++;
} else {
$scheda = AssistenzaTecnorepairScheda::query()->create($payload);
$stats['schede_create']++;
}
// Align ProductSerial
$serial = ProductSerial::query()->updateOrCreate(
['legacy_scheda_id' => (int) $scheda->id],
[
'fornitore_id' => $fornitore->id,
'customer_name' => $scheda->customer_name,
'product_model' => $scheda->product_model,
'product_code' => $scheda->product_code,
'serial_number' => $scheda->serial_number,
'serial_number_2' => $scheda->serial_number_2,
'date_received' => $scheda->date_received,
'internal_notes' => $scheda->communications,
'source' => 'tecnorepair_mdb',
'source_reference' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id),
]
);
$stats['seriali_allineati']++;
$this->catalogService->syncTecnorepairProduct($fornitore, $scheda, $serial);
// Allegati
AssistenzaTecnorepairAllegato::query()->where('scheda_id', (int) $scheda->id)->delete();
foreach ($allegatiByScheda[$legacyId] ?? [] as $allegatoRow) {
AssistenzaTecnorepairAllegato::query()->create([
'scheda_id' => (int) $scheda->id,
'legacy_id' => $this->toInt($allegatoRow['ID'] ?? null),
'legacy_scheda_id' => $legacyId,
'legacy_attachment_number' => $this->toInt($allegatoRow['NumAllegato'] ?? null),
'file_name' => basename((string) ($allegatoRow['FileAllegato'] ?? 'allegato')),
'file_path' => $this->clean($allegatoRow['FileAllegato'] ?? null),
'imported_from_path' => $resolvedPath,
'metadata' => ['raw' => $allegatoRow],
]);
$stats['allegati_importati']++;
}
}
};
if ($dryRun) {
$runner();
} else {
DB::transaction($runner);
// Update Fornitore operational_config with last sync info
$config = (array) ($fornitore->operational_config ?? []);
$config['tecnorepair']['last_sync'] = [
'timestamp' => now()->toIso8601String(),
'resolved_path' => $resolvedPath,
'schede_lette' => $stats['schede_lette'],
'schede_create' => $stats['schede_create'],
'schede_aggiornate' => $stats['schede_aggiornate'],
'ricambi_catalogati' => $stats['ricambi_catalogati'],
'seriali_allineati' => $stats['seriali_allineati'],
];
$fornitore->operational_config = $config;
$fornitore->save();
}
return $stats;
}
/**
* Automated Workflow 1: Mark repair as completed (RIPARATO).
*/
public function chiudiComeRiparato(
AssistenzaTecnorepairScheda $scheda,
?string $note = null,
?string $tecnico = null
): bool {
$meta = (array) ($scheda->metadata ?? []);
$meta['workflow_status'] = 'repaired';
$meta['repaired_at'] = now()->toIso8601String();
$scheda->status_code = '4';
$scheda->status_label = 'RIPARATO';
$scheda->status_bucket = 'completed';
if ($tecnico) {
$scheda->technician_name = $tecnico;
}
if ($note) {
$prevNotes = trim((string) $scheda->repair_description);
$scheda->repair_description = $prevNotes !== ''
? ($prevNotes . "\n[" . now()->format('d/m/Y H:i') . "] " . $note)
: $note;
}
$scheda->metadata = $meta;
return $scheda->save();
}
/**
* Automated Workflow 2: Mark device as scrapped or used for spare parts (DISPOSITIVO IRRIPARABILE / PZ-RICAM).
*/
public function chiudiComeRottamato(
AssistenzaTecnorepairScheda $scheda,
?string $motivo = null,
bool $comeRicambi = false
): bool {
$meta = (array) ($scheda->metadata ?? []);
$meta['workflow_status'] = $comeRicambi ? 'spare_parts' : 'scrapped';
$meta['scrapped_at'] = now()->toIso8601String();
$meta['scrap_reason'] = $motivo;
if ($comeRicambi) {
$scheda->status_code = '23';
$scheda->status_label = 'PZ-RICAM - NOTEBOOK PC DA UTILIZZARE COME RICAMBI';
$scheda->status_bucket = 'scrapped';
} else {
$scheda->status_code = '10';
$scheda->status_label = 'DISPOSITIVO IRRIPARABILE';
$scheda->status_bucket = 'cancelled';
}
$desc = trim((string) $scheda->repair_description);
$append = "Apparato contrassegnato per " . ($comeRicambi ? 'recupero ricambi' : 'rottamazione/smaltimento') . ". Motivo: " . ($motivo ?: 'Non riparabile economicamente');
$scheda->repair_description = $desc !== '' ? ($desc . "\n[" . now()->format('d/m/Y H:i') . "] " . $append) : $append;
$scheda->metadata = $meta;
return $scheda->save();
}
/**
* Automated Workflow 3: Return to vendor / Warranty RMA (MANDATO IN GARANZIA AL PRODUTTORE).
*/
public function rendiAFornitoreRma(
AssistenzaTecnorepairScheda $scheda,
?string $rmaCode = null,
?int $fornitoreResoId = null,
?string $note = null
): bool {
$meta = (array) ($scheda->metadata ?? []);
$generatedRma = $rmaCode ?: ('RMA-' . date('Ymd') . '-' . ($scheda->legacy_numero_scheda ?: $scheda->id));
$meta['workflow_status'] = 'vendor_rma';
$meta['rma_initiated_at'] = now()->toIso8601String();
$meta['rma_fornitore_id'] = $fornitoreResoId;
$scheda->status_code = '33';
$scheda->status_label = 'MANDATO IN GARANZIA AL PRODUTTORE';
$scheda->status_bucket = 'in_progress';
$scheda->rma_code = $generatedRma;
$comm = trim((string) $scheda->communications);
$append = "Inviato in RMA / Garanzia fornitore. Codice RMA: {$generatedRma}." . ($note ? " Note: {$note}" : '');
$scheda->communications = $comm !== '' ? ($comm . "\n[" . now()->format('d/m/Y H:i') . "] " . $append) : $append;
$scheda->metadata = $meta;
return $scheda->save();
}
private function clean(mixed $value): ?string
{
if (! is_string($value) && ! is_numeric($value)) {
return null;
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function toInt(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
if (! is_numeric($value)) {
return null;
}
return (int) $value;
}
private function normalizeDate(mixed $value): ?string
{
$value = $this->clean($value);
if ($value === null) {
return null;
}
try {
return Carbon::parse($value)->toDateTimeString();
} catch (\Throwable) {
return null;
}
}
}