259 lines
9.7 KiB
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();
|
|
}
|
|
}
|