netgescon-day0/app/Services/ContatoriImportService.php

259 lines
9.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\UnitaImmobiliare;
use App\Models\StabileServizioLettura;
use Illuminate\Support\Facades\DB;
use Exception;
class ContatoriImportService
{
/**
* Import readings from a CSV file.
* Expected CSV format: matricola, data_lettura, valore_lettura, [interno]
*
* @param int $stabileId
* @param string $csvPath
* @param string $fileName
* @return array{imported: int, staged: int, errors: array}
*/
public function importFromCsv(int $stabileId, string $csvPath, string $fileName): array
{
$importedCount = 0;
$stagedCount = 0;
$errors = [];
if (!file_exists($csvPath) || !is_readable($csvPath)) {
$errors[] = "File non accessibile o illeggibile.";
return ['imported' => 0, 'staged' => 0, 'errors' => $errors];
}
$handle = fopen($csvPath, 'r');
if (!$handle) {
$errors[] = "Impossibile aprire il file CSV.";
return ['imported' => 0, 'staged' => 0, 'errors' => $errors];
}
// Detect delimiter: CSV could be comma or semicolon separated
$header = fgetcsv($handle, 1000, ';');
if (count($header) <= 1) {
rewind($handle);
$header = fgetcsv($handle, 1000, ',');
$delimiter = ',';
} else {
$delimiter = ';';
}
if (empty($header)) {
$errors[] = "File CSV vuoto o non conforme.";
fclose($handle);
return ['imported' => 0, 'staged' => 0, 'errors' => $errors];
}
// Normalize header columns to lowercase
$header = array_map(fn($col) => strtolower(trim((string)$col)), $header);
// Find positions of fields
$posMatricola = array_search('matricola', $header) !== false
? array_search('matricola', $header)
: (array_search('#id', $header) !== false
? array_search('#id', $header)
: (array_search('id', $header) !== false
? array_search('id', $header)
: 0));
$posData = array_search('data_lettura', $header) !== false ? array_search('data_lettura', $header) : 1;
$posValore = array_search('valore_lettura', $header) !== false
? array_search('valore_lettura', $header)
: (array_search('lettura', $header) !== false
? array_search('lettura', $header)
: 2);
$posInterno = array_search('interno', $header) !== false ? array_search('interno', $header) : 3;
$servizio = DB::table('stabile_servizi')
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->first();
$servizioId = $servizio?->id;
if (!$servizioId) {
$errors[] = "Servizio acqua non configurato per questo stabile.";
fclose($handle);
return ['imported' => 0, 'staged' => 0, 'errors' => $errors];
}
while (($row = fgetcsv($handle, 1000, $delimiter)) !== false) {
if (empty($row) || count($row) < 3) {
continue;
}
$rawMatricola = trim((string)($row[$posMatricola] ?? ''));
$rawData = trim((string)($row[$posData] ?? ''));
$rawValore = trim((string)($row[$posValore] ?? ''));
$rawInterno = isset($row[$posInterno]) ? trim((string)$row[$posInterno]) : null;
if ($rawMatricola === '' || $rawValore === '') {
continue;
}
// Parse date
$dataLettura = null;
try {
if (str_contains($rawData, '/')) {
$parts = explode('/', $rawData);
if (count($parts) === 3) {
$day = (int)$parts[0];
$month = (int)$parts[1];
$year = (int)$parts[2];
if ($year < 100) {
$year += ($year >= 70) ? 1900 : 2000;
}
$dataLettura = sprintf("%04d-%02d-%02d", $year, $month, $day);
}
} else {
$dataLettura = date('Y-m-d', strtotime($rawData));
}
} catch (Exception) {
$dataLettura = now()->toDateString();
}
if (!$dataLettura) {
$dataLettura = now()->toDateString();
}
// Parse value
$valoreLettura = (float) str_replace(',', '.', $rawValore);
// Find matching unit by serial number or gateway device
$unit = UnitaImmobiliare::where('stabile_id', $stabileId)
->where(function ($query) use ($rawMatricola) {
$query->where('acqua_contatore_seriale', $rawMatricola)
->orWhere('acqua_gateway_device_id', $rawMatricola);
})
->first();
if (!$unit && $rawInterno !== null && $rawInterno !== '') {
$unit = UnitaImmobiliare::where('stabile_id', $stabileId)
->where('interno', $rawInterno)
->first();
if ($unit) {
$unit->update([
'acqua_contatore_seriale' => $rawMatricola,
'acqua_gateway_device_id' => $rawMatricola,
]);
}
}
if ($unit) {
$latestReal = StabileServizioLettura::query()
->where('unita_immobiliare_id', $unit->id)
->where('stabile_servizio_id', $servizioId)
->orderByDesc('periodo_al')
->first();
StabileServizioLettura::create([
'stabile_id' => $stabileId,
'stabile_servizio_id' => $servizioId,
'unita_immobiliare_id' => $unit->id,
'periodo_dal' => $latestReal?->periodo_al ?? $dataLettura,
'periodo_al' => $dataLettura,
'tipologia_lettura' => 'elettronica_remota',
'canale_acquisizione' => 'csv_import',
'workflow_stato' => 'rilevata',
'lettura_precedente_valore' => $latestReal?->lettura_fine ?? 0.0,
'lettura_fine' => $valoreLettura,
'consumo_valore' => max(0, $valoreLettura - ($latestReal?->lettura_fine ?? 0.0)),
'consumo_unita' => 'mc',
'raw' => ['file_name' => $fileName, 'raw_row' => $row],
'created_by' => auth()->id() ?: 1,
]);
$importedCount++;
} else {
DB::table('contatori_orfani_staging')->insert([
'stabile_id' => $stabileId,
'matricola' => $rawMatricola,
'interno_originale' => $rawInterno,
'data_lettura' => $dataLettura,
'valore_lettura' => $valoreLettura,
'file_name' => $fileName,
'created_at' => now(),
'updated_at' => now(),
]);
$stagedCount++;
}
}
fclose($handle);
return [
'imported' => $importedCount,
'staged' => $stagedCount,
'errors' => $errors
];
}
/**
* Associate an orphan serial number to a unit, learn it, and migrate the reading(s).
*
* @param int $orphanId
* @param int $unitId
* @return void
*/
public function associateOrphan(int $orphanId, int $unitId): void
{
$orphan = DB::table('contatori_orfani_staging')->where('id', $orphanId)->first();
if (!$orphan) {
return;
}
$unit = UnitaImmobiliare::find($unitId);
if (!$unit) {
return;
}
// 1. Auto-learn: save the serial directly to the unit
$unit->update([
'acqua_contatore_seriale' => $orphan->matricola,
'acqua_gateway_device_id' => $orphan->matricola,
]);
// 2. Migrate the reading to stabile_servizio_letture
$servizio = DB::table('stabile_servizi')
->where('stabile_id', $unit->stabile_id)
->where('tipo', 'acqua')
->first();
$servizioId = $servizio?->id;
if ($servizioId) {
$latestReal = StabileServizioLettura::query()
->where('unita_immobiliare_id', $unit->id)
->where('stabile_servizio_id', $servizioId)
->orderByDesc('periodo_al')
->first();
StabileServizioLettura::create([
'stabile_id' => $unit->stabile_id,
'stabile_servizio_id' => $servizioId,
'unita_immobiliare_id' => $unit->id,
'periodo_dal' => $latestReal?->periodo_al ?? $orphan->data_lettura,
'periodo_al' => $orphan->data_lettura,
'tipologia_lettura' => 'elettronica_remota',
'canale_acquisizione' => 'csv_import_associated',
'workflow_stato' => 'rilevata',
'lettura_precedente_valore' => $latestReal?->lettura_fine ?? 0.0,
'lettura_fine' => $orphan->valore_lettura,
'consumo_valore' => max(0, $orphan->valore_lettura - ($latestReal?->lettura_fine ?? 0.0)),
'consumo_unita' => 'mc',
'raw' => ['orphan_id' => $orphan->id, 'matricola_originale' => $orphan->matricola],
'created_by' => auth()->id() ?: 1,
]);
}
// 3. Delete from staging
DB::table('contatori_orfani_staging')->where('id', $orphanId)->delete();
}
}