HEX
Server: nginx/1.27.0
System: Linux ns31201788 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User: www-data (33)
PHP: 8.1.31
Disabled: NONE
Upload Files
File: //tmp/rc_backup.php
<?php

class ReceiptController extends BaseController
{
    public function index(): void
    {
        $this->requireAuth();
        $status = $this->input('status');
        $search = $this->input('q');

        $where = "1=1";
        $params = [];

        if ($status) {
            $where .= " AND r.status = ?";
            $params[] = $status;
        }
        if ($search) {
            $where .= " AND (r.riferimento_testo LIKE ? OR r.riferimento_numero LIKE ? OR c.company_name LIKE ?)";
            $params[] = "%{$search}%";
            $params[] = "%{$search}%";
            $params[] = "%{$search}%";
        }

        $receipts = Database::fetchAll(
            "SELECT r.*, c.company_name, c.cust_code 
             FROM receipts r 
             LEFT JOIN customers c ON r.customer_id = c.id 
             WHERE {$where} 
             ORDER BY r.created_at DESC 
             LIMIT 100",
            $params
        );

        $this->view('receipts/index', [
            'pageTitle' => 'Scontrini / Pratiche',
            'receipts'  => $receipts,
            'status'    => $status,
            'search'    => $search,
        ]);
    }

    public function show(string $id): void
    {
        $this->requireAuth();
        $receipt = Database::fetchOne(
            "SELECT r.*, c.company_name, c.cust_code, c.tax_id_number, c.fiscal_code, c.address, c.city, c.zip_code, c.county, c.ipa_code, c.ei_certified_email, c.telephone, c.email
             FROM receipts r LEFT JOIN customers c ON r.customer_id = c.id WHERE r.id = ?", [$id]
        );
        if (!$receipt) {
            $this->flash('error', 'Pratica non trovata');
            $this->redirect('/receipts');
        }

        $items = Database::fetchAll("SELECT * FROM receipt_items WHERE receipt_id = ? ORDER BY line_number", [$id]);
        $installments = Database::fetchAll(
            "SELECT * FROM receipt_installments WHERE receipt_id = ? ORDER BY installment_number", [$id]
        );

        $this->view('receipts/show', [
            'pageTitle'    => 'Pratica #' . $receipt['riferimento_numero'],
            'receipt'      => $receipt,
            'items'        => $items,
            'installments' => $installments,
        ]);
    }

    public function create(): void
    {
        $this->requireAuth();
        $this->view('receipts/form', [
            'pageTitle' => 'Nuova Pratica',
            'receipt'   => null,
        ]);
    }

    public function edit(string $id): void
    {
        $this->requireAuth();
        $receipt = Database::fetchOne("SELECT * FROM receipts WHERE id = ?", [$id]);
        if (!$receipt) {
            $this->flash('error', 'Pratica non trovata');
            $this->redirect('/receipts');
        }
        $items = Database::fetchAll("SELECT * FROM receipt_items WHERE receipt_id = ? ORDER BY line_number", [$id]);
        $this->view('receipts/form', [
            'pageTitle' => 'Modifica Pratica',
            'receipt'   => $receipt,
            'items'     => $items,
        ]);
    }

    public function confirm(string $id): void
    {
        $this->requireAuth();
        $receipt = Database::fetchOne("SELECT * FROM receipts WHERE id = ?", [$id]);
        if (!$receipt || $receipt['status'] !== 'draft') {
            $this->flash('error', 'Pratica non confermabile');
            $this->redirect('/receipts/' . $id);
            return;
        }
        if (empty($receipt['customer_id'])) {
            $this->flash('error', 'Associa un cliente prima di confermare');
            $this->redirect('/receipts/' . $id);
            return;
        }

        Database::beginTransaction();
        try {
            // Genera piano rate
            $numRate = (int)($receipt['num_installments'] ?? 3);
            $advance = (float)($receipt['advance_amount'] ?? 0);
            $toFinance = (float)($receipt['total'] ?? 0) - $advance;
            $costRetail = (float)($receipt['dilazione_cost'] ?? 0);
            $totalRate = round($toFinance, 2);
            $instAmount = $numRate > 0 ? round($totalRate / $numRate, 2) : $totalRate;

            // Data base = data scontrino
            $receipt_dates = Database::fetchOne("SELECT riferimento_data FROM receipts WHERE id = ?", [$id]);
            $today = !empty($receipt_dates["riferimento_data"]) ? new \DateTime($receipt_dates["riferimento_data"]) : new \DateTime();
            $reminderDays = 15;
            try { $reminderDays = (int)(Database::fetchOne("SELECT setting_value FROM settings WHERE setting_key = 'reminder_days_before'")['setting_value'] ?? 15); } catch(\Throwable $e) {}

            // Rimuovi rate esistenti
            Database::delete('receipt_installments', 'receipt_id = ?', [$id]);

            for ($i = 1; $i <= $numRate; $i++) {
                $days = ($i - 1) * 30;
                $dueDate = (clone $today)->modify("+{$days} days");
                $reminderDate = (clone $dueDate)->modify("-{$reminderDays} days");
                $amount = $i === $numRate ? round($totalRate - ($instAmount * ($numRate - 1)), 2) : $instAmount;

                Database::insert('receipt_installments', [
                    'receipt_id'         => $id,
                    'installment_number' => $i,
                    'due_date'           => $dueDate->format('Y-m-d'),
                    'amount'             => $amount,
                    'paid_amount'        => 0,
                    'status'             => 'pending',
                    'reminder_date'      => $reminderDate->format('Y-m-d'),
                ]);
            }

            Database::update('receipts', [
                'status'     => 'confirmed',
                'updated_at' => date('Y-m-d H:i:s'),
            ], 'id = ?', [$id]);

            Database::commit();
            $this->log('receipt_confirmed', 'receipts', (int)$id, ['installments' => $numRate]);
            $this->flash('success', "Pratica confermata con {$numRate} rate generate");

        } catch (\Throwable $e) {
            Database::rollBack();
            $this->flash('error', 'Errore conferma: ' . $e->getMessage());
        }

        $this->redirect('/receipts/' . $id);
    }

    public function assignCustomer(string $id): void
    {
        $this->requireAuth();
        $customerId = (int)$this->input("customer_id");
        if (!$customerId) { $this->flash("error", "Cliente non valido"); $this->redirect("/receipts/" . $id); return; }
        Database::update("receipts", ["customer_id" => $customerId, "updated_at" => date("Y-m-d H:i:s")], "id = ?", [$id]);
        $this->log("customer_assigned", "receipts", (int)$id, ["customer_id" => $customerId]);
        $this->flash("success", "Cliente associato alla pratica");
        $this->redirect("/receipts/" . $id);
    }

    public function configure(string $id): void
    {
        $this->requireAuth();
        $receipt = Database::fetchOne("SELECT * FROM receipts WHERE id = ?", [$id]);
        if (!$receipt || $receipt["status"] !== "draft") { $this->flash("error", "Pratica non modificabile"); $this->redirect("/receipts/" . $id); return; }

        $advMode = $this->input("advance_mode", "percent");
        $advValue = (float)$this->input("advance_value", 50);
        $numRate = (int)$this->input("num_installments", 3);
        $subtotal = (float)$receipt["subtotal"];

        if ($advMode === "fixed") {
            $advance = min($advValue, $subtotal);
            $advPercent = $subtotal > 0 ? round(($advance / $subtotal) * 100, 2) : 0;
        } else {
            $advPercent = min($advValue, 100);
            $advance = round($subtotal * ($advPercent / 100), 2);
        }

        $toFinance = round($subtotal - $advance, 2);
        $condition = Database::fetchOne("SELECT * FROM payment_conditions WHERE amount_min <= ? AND amount_max >= ? AND num_installments = ? AND active = 1", [$subtotal, $subtotal, $numRate]);
        $costRetail = $condition ? (float)$condition["cost_retail"] : 0;
        $total = round($toFinance + $costRetail, 2);
        $instAmount = $numRate > 0 ? round($total / $numRate, 2) : $total;

        Database::update("receipts", [
            "advance_mode" => $advMode, "advance_percent" => $advPercent, "advance_amount" => $advance,
            "num_installments" => $numRate, "dilazione_cost" => $costRetail, "total" => round($subtotal + $costRetail, 2),
            "installment_amount" => $instAmount, "payment_code" => $condition ? $condition["code"] : null,
            "updated_at" => date("Y-m-d H:i:s"),
        ], "id = ?", [$id]);

        $this->flash("success", "Pagamento configurato: acconto " . number_format($advance, 2) . ", " . $numRate . " rate da " . number_format($instAmount, 2));
        $this->redirect("/receipts/" . $id);
    }

    public function delete(string $id): void
    {
        $this->requireRole("admin");
        $receipt = Database::fetchOne("SELECT * FROM receipts WHERE id = ?", [$id]);
        if (!$receipt) {
            $this->flash("error", "Pratica non trovata");
            $this->redirect("/receipts");
            return;
        }
        Database::beginTransaction();
        try {
            Database::delete("installment_payments", "installment_id IN (SELECT id FROM receipt_installments WHERE receipt_id = ?)", [$id]);
            Database::delete("receipt_installments", "receipt_id = ?", [$id]);
            Database::delete("receipt_items", "receipt_id = ?", [$id]);
            Database::delete("receipts", "id = ?", [$id]);
            Database::commit();
            $this->log("receipt_deleted", "receipts", (int)$id);
            $this->flash("success", "Pratica #" . $receipt["riferimento_numero"] . " eliminata");
        } catch (\Throwable $e) {
            Database::rollBack();
            $this->flash("error", "Errore: " . $e->getMessage());
        }
        $this->redirect("/receipts");
    }

    public function importCsv(): void
    {
        $this->requireAuth();

        if (empty($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
            $this->flash('error', 'Errore nel caricamento del file CSV');
            $this->redirect('/receipts');
            return;
        }

        $file = $_FILES['csv_file']['tmp_name'];
        $filename = $_FILES['csv_file']['name'];

        try {
            $handle = fopen($file, 'r');
            if (!$handle) throw new \Exception('Impossibile aprire il file');

            // Leggi header
            $headerLine = fgets($handle);
            if (!$headerLine) throw new \Exception('File vuoto');

            // Detect separatore (punto e virgola)
            $separator = ';';

            // Parse header
            $headers = array_map('trim', explode($separator, $headerLine));

            // Mappa colonne (case-insensitive)
            $colMap = [];
            foreach ($headers as $i => $h) {
                $h = strtolower(trim($h));
                $colMap[$h] = $i;
            }

            // Verifica colonne necessarie
            $requiredFields = ['negozio', 'descrizione', 'num matricola', 'num scontrino', 'data'];
            foreach ($requiredFields as $rf) {
                if (!isset($colMap[$rf])) {
                    throw new \Exception("Colonna obbligatoria mancante: '{$rf}'. Colonne trovate: " . implode(', ', $headers));
                }
            }

            // Leggi righe dati
            $rows = [];
            $lineNum = 1;
            while (($line = fgets($handle)) !== false) {
                $lineNum++;
                $line = trim($line);
                if (empty($line)) continue;

                $cols = explode($separator, $line);
                // Rimuovi ultimo elemento se vuoto (trailing semicolon)
                if (end($cols) === '' || end($cols) === false) array_pop($cols);

                $row = [];
                $row['negozio']          = trim($cols[$colMap['negozio']] ?? '');
                $row['barcode']          = trim($cols[$colMap['barcode'] ?? -1] ?? '');
                $row['qta']              = (int)trim($cols[$colMap['qta'] ?? -1] ?? '1');
                $row['listino']          = (float)str_replace(',', '.', trim($cols[$colMap['listino'] ?? -1] ?? '0'));
                $row['sconto_val']       = (float)str_replace(',', '.', trim($cols[$colMap['sconto val'] ?? -1] ?? '0'));
                $row['descrizione']      = trim($cols[$colMap['descrizione']] ?? '');
                $row['num_matricola']    = trim($cols[$colMap['num matricola']] ?? '');
                $row['num_scontrino']    = trim($cols[$colMap['num scontrino']] ?? '');
                $row['codice_cliente']   = trim($cols[$colMap['codice cliente'] ?? -1] ?? '');
                $row['codice_iva']       = (float)trim($cols[$colMap['codice iva'] ?? -1] ?? '22');
                $row['non_riscosso']     = (float)str_replace(',', '.', trim($cols[$colMap['non riscosso'] ?? -1] ?? '0'));
                $row['causale']          = trim($cols[$colMap['causale'] ?? -1] ?? 'VE');
                $row['data']             = trim($cols[$colMap['data']] ?? '');

                if (empty($row['descrizione']) && empty($row['barcode'])) continue;
                $rows[] = $row;
            }
            fclose($handle);

            if (empty($rows)) throw new \Exception('Nessuna riga dati valida trovata nel CSV');

            // Raggruppa per num_matricola + num_scontrino (= 1 pratica)
            $groups = [];
            foreach ($rows as $row) {
                $key = $row['num_matricola'] . '|' . $row['num_scontrino'];
                $groups[$key][] = $row;
            }

            $importedCount = 0;

            Database::beginTransaction();

            foreach ($groups as $key => $groupRows) {
                $first = $groupRows[0];

                // Parse data
                $dataStr = $first['data'];
                $receiptDate = null;
                // Formato: dd/mm/yyyy HH:ii:ss
                if (preg_match('#(\d{2})/(\d{2})/(\d{4})#', $dataStr, $m)) {
                    $receiptDate = "{$m[3]}-{$m[2]}-{$m[1]}";
                }

                // Verifica se esiste gia
                // Deduplicazione per filename + matricola + scontrino
                $existing = Database::fetchOne(
                    "SELECT id FROM receipts WHERE csv_filename = ? AND riferimento_testo = ? AND riferimento_numero = ?",
                    [$filename, $first['num_matricola'], $first['num_scontrino']]
                );
                if ($existing) continue; // Salta duplicati

                // Cerca cliente per codice
                $customer = null;
                if (!empty($first['codice_cliente'])) {
                    $customer = Database::fetchOne(
                        "SELECT id FROM customers WHERE contact_person = ? OR fiscal_code = ? OR cust_code = ?",
                        [$first['codice_cliente'], $first['codice_cliente'], $first['codice_cliente']]
                    );
                }

                // Calcola totale e non riscosso
                $subtotal = 0;
                $totalNonRiscosso = 0;
                foreach ($groupRows as $row) {
                    $lineTotal = ($row['listino'] * $row['qta']) - $row['sconto_val'];
                    $subtotal += $lineTotal;
                    $totalNonRiscosso += $row['non_riscosso'];
                }
                $contestuale = round($subtotal - $totalNonRiscosso, 2);

                // Crea receipt
                $receiptId = Database::insert('receipts', [
                    'customer_id'         => $customer ? $customer['id'] : null,
                    'riferimento_testo'   => $first['num_matricola'],
                    'riferimento_numero'  => $first['num_scontrino'],
                    'riferimento_data'    => $receiptDate,
                    'store_code'          => $first['negozio'],
                    'client_code_csv'     => $first['codice_cliente'],
                    'subtotal'            => $subtotal,
                    'total'               => $subtotal,
                    'non_riscosso'        => $totalNonRiscosso,
                    'advance_amount'      => $contestuale,
                    'invoice_reason'      => 'S-VEND',
                    'ei_document_type'    => 'TD24',
                    'status'              => 'draft',
                    'csv_filename'        => $filename,
                    'created_at'          => date('Y-m-d H:i:s'),
                    'updated_at'          => date('Y-m-d H:i:s'),
                ]);

                // Crea righe
                foreach ($groupRows as $lineNum => $row) {
                    Database::insert('receipt_items', [
                        'receipt_id'       => $receiptId,
                        'line_number'      => $lineNum + 1,
                        'line_type'        => 'Merce',
                        'barcode'          => $row['barcode'],
                        'description'      => $row['descrizione'],
                        'uom'              => 'NR',
                        'quantity'         => $row['qta'],
                        'unit_value'       => $row['listino'],
                        'discount_formula' => $row['sconto_val'] > 0 ? number_format($row['sconto_val'], 2) : null,
                        'tax_rate'         => $row['codice_iva'],
                        'line_total'       => ($row['listino'] * $row['qta']) - $row['sconto_val'],
                    ]);
                }

                $this->log('receipt_imported', 'receipts', $receiptId, [
                    'file' => $filename,
                    'lines' => count($groupRows),
                ]);

                $importedCount++;
            }

            Database::commit();

            $totalRows = count($rows);
            $this->flash('success', "Import completato: {$importedCount} pratiche create da {$totalRows} righe (file: {$filename})");

        } catch (\Exception $e) {
            if (Database::inTransaction()) Database::rollBack();
            $this->flash('error', 'Errore import CSV: ' . $e->getMessage());
        }

        $this->redirect('/receipts');
    }

    public function updateCustomer(string $id): void
    {
        $this->requireAuth();
        
        $receipt = Database::fetchOne("SELECT customer_id FROM receipts WHERE id = ?", [$id]);
        if (!$receipt || !$receipt['customer_id']) {
            $this->flash('error', 'Nessun cliente associato');
            $this->redirect('/receipts/' . $id);
            return;
        }
        
        $data = [
            'company_name'        => trim($_POST['company_name'] ?? ''),
            'tax_id_number'       => trim($_POST['tax_id_number'] ?? ''),
            'fiscal_code'         => trim($_POST['fiscal_code'] ?? ''),
            'address'             => trim($_POST['address'] ?? ''),
            'city'                => trim($_POST['city'] ?? ''),
            'zip_code'            => trim($_POST['zip_code'] ?? ''),
            'county'              => trim($_POST['county'] ?? ''),
            'ipa_code'            => trim($_POST['ipa_code'] ?? ''),
            'ei_certified_email'  => trim($_POST['ei_certified_email'] ?? ''),
            'telephone'           => trim($_POST['telephone'] ?? ''),
            'email'               => trim($_POST['email'] ?? ''),
            'updated_at'          => date('Y-m-d H:i:s'),
        ];
        
        Database::update('customers', $data, 'id = ?', [$receipt['customer_id']]);
        
        $this->log('customer_updated', 'customers', $receipt['customer_id'], $data);
        $this->flash('success', 'Dati cliente aggiornati');
        $this->redirect('/receipts/' . $id);
    }

    public function scanFolder(): void
    {
        $this->requireAuth();
        header('Content-Type: application/json; charset=utf-8');
        
        $folder = '/mnt/fattmagoel/Fattmagoel';
        $files = [];
        
        if (is_dir($folder)) {
            foreach (glob($folder . '/fattmagoel_*.csv') as $filepath) {
                $filename = basename($filepath);
                $already = Database::fetchOne(
                    "SELECT id FROM csv_imports WHERE filename = ?", [$filename]
                );
                if (!$already) {
                    $files[] = [
                        'filename' => $filename,
                        'filepath' => $filepath,
                        'size'     => filesize($filepath),
                        'date'     => date('Y-m-d H:i:s', filemtime($filepath)),
                    ];
                }
            }
        }
        
        echo json_encode(['files' => $files, 'count' => count($files)], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function importFromFolder(): void
    {
        $this->requireAuth();
        
        $folder = '/mnt/fattmagoel/Fattmagoel';
        $filename = $_POST['filename'] ?? '';
        
        if (empty($filename)) {
            $this->flash('error', 'Nessun file selezionato');
            $this->redirect('/receipts');
            return;
        }
        
        $filepath = $folder . '/' . basename($filename);
        
        if (!file_exists($filepath)) {
            $this->flash('error', 'File non trovato: ' . $filename);
            $this->redirect('/receipts');
            return;
        }
        
        // Simula upload CSV riusando importCsv logic
        $_FILES['csv_file'] = [
            'tmp_name' => $filepath,
            'name'     => $filename,
            'error'    => UPLOAD_ERR_OK,
        ];
        
        // Prima dell'import, salva in csv_imports
        try {
            $this->importCsv();
            
            // Se arriviamo qui senza redirect, salva risultato
            Database::insert('csv_imports', [
                'filename'         => $filename,
                'filepath'         => $filepath,
                'file_date'        => date('Y-m-d H:i:s', filemtime($filepath)),
                'status'           => 'imported',
                'imported_at'      => date('Y-m-d H:i:s'),
            ]);
        } catch (\Exception $e) {
            Database::insert('csv_imports', [
                'filename'         => $filename,
                'filepath'         => $filepath,
                'file_date'        => date('Y-m-d H:i:s', filemtime($filepath)),
                'status'           => 'error',
                'error_message'    => $e->getMessage(),
                'imported_at'      => date('Y-m-d H:i:s'),
            ]);
        }
    }

    public function importAllFromFolder(): void
    {
        $this->requireAuth();
        
        $folder = '/mnt/fattmagoel/Fattmagoel';
        $imported = 0;
        $errors = 0;
        $errorMsgs = [];
        
        if (is_dir($folder)) {
            foreach (glob($folder . '/fattmagoel_*.csv') as $filepath) {
                $filename = basename($filepath);
                $already = Database::fetchOne(
                    "SELECT id FROM csv_imports WHERE filename = ?", [$filename]
                );
                if ($already) continue;
                
                try {
                    $result = $this->processCSVFile($filepath, $filename);
                    
                    Database::insert('csv_imports', [
                        'filename'         => $filename,
                        'filepath'         => $filepath,
                        'file_date'        => date('Y-m-d H:i:s', filemtime($filepath)),
                        'receipts_created' => $result['receipts'],
                        'rows_total'       => $result['rows'],
                        'status'           => 'imported',
                        'imported_at'      => date('Y-m-d H:i:s'),
                    ]);
                    $imported++;
                } catch (\Exception $e) {
                    Database::insert('csv_imports', [
                        'filename'      => $filename,
                        'filepath'      => $filepath,
                        'file_date'     => date('Y-m-d H:i:s', filemtime($filepath)),
                        'status'        => 'error',
                        'error_message' => $e->getMessage(),
                        'imported_at'   => date('Y-m-d H:i:s'),
                    ]);
                    $errors++;
                    $errorMsgs[] = $filename . ': ' . $e->getMessage();
                }
            }
        }
        
        $msg = "Auto-import: {$imported} file importati";
        if ($errors > 0) $msg .= ", {$errors} errori (" . implode('; ', $errorMsgs) . ")";
        $this->flash($errors > 0 ? 'warning' : 'success', $msg);
        $this->redirect('/receipts');
    }
    
    private function processCSVFile(string $filepath, string $filename): array
    {
        $handle = fopen($filepath, 'r');
        if (!$handle) throw new \Exception('Impossibile aprire il file');

        $headerLine = fgets($handle);
        if (!$headerLine) throw new \Exception('File vuoto');

        $separator = ';';
        $headers = array_map('trim', explode($separator, $headerLine));
        $colMap = [];
        foreach ($headers as $i => $h) {
            $colMap[strtolower(trim($h))] = $i;
        }

        $requiredFields = ['negozio', 'descrizione', 'num matricola', 'num scontrino', 'data'];
        foreach ($requiredFields as $rf) {
            if (!isset($colMap[$rf])) {
                throw new \Exception("Colonna obbligatoria mancante: '{$rf}'");
            }
        }

        $rows = [];
        while (($line = fgets($handle)) !== false) {
            $line = trim($line);
            if (empty($line)) continue;

            $cols = explode($separator, $line);
            if (end($cols) === '' || end($cols) === false) array_pop($cols);

            $row = [];
            $row['negozio']          = trim($cols[$colMap['negozio']] ?? '');
            $row['barcode']          = trim($cols[$colMap['barcode'] ?? -1] ?? '');
            $row['qta']              = (int)trim($cols[$colMap['qta'] ?? -1] ?? '1');
            $row['listino']          = (float)str_replace(',', '.', trim($cols[$colMap['listino'] ?? -1] ?? '0'));
            $row['sconto_val']       = (float)str_replace(',', '.', trim($cols[$colMap['sconto val'] ?? -1] ?? '0'));
            $row['descrizione']      = trim($cols[$colMap['descrizione']] ?? '');
            $row['num_matricola']    = trim($cols[$colMap['num matricola']] ?? '');
            $row['num_scontrino']    = trim($cols[$colMap['num scontrino']] ?? '');
            $row['codice_cliente']   = trim($cols[$colMap['codice cliente'] ?? -1] ?? '');
            $row['codice_iva']       = (float)trim($cols[$colMap['codice iva'] ?? -1] ?? '22');
            $row['non_riscosso']     = (float)str_replace(',', '.', trim($cols[$colMap['non riscosso'] ?? -1] ?? '0'));
            $row['causale']          = trim($cols[$colMap['causale'] ?? -1] ?? 'VE');
            $row['data']             = trim($cols[$colMap['data']] ?? '');

            if (empty($row['descrizione']) && empty($row['barcode'])) continue;
            $rows[] = $row;
        }
        fclose($handle);

        if (empty($rows)) throw new \Exception('Nessuna riga dati valida');

        $groups = [];
        foreach ($rows as $row) {
            $key = $row['num_matricola'] . '|' . $row['num_scontrino'];
            $groups[$key][] = $row;
        }

        $importedCount = 0;
        Database::beginTransaction();

        foreach ($groups as $key => $groupRows) {
            $first = $groupRows[0];

            $receiptDate = null;
            if (preg_match('#(\d{2})/(\d{2})/(\d{4})#', $first['data'], $m)) {
                $receiptDate = "{$m[3]}-{$m[2]}-{$m[1]}";
            }

            // Deduplicazione per filename (ogni file = 1 pratica unica)
            $existing = Database::fetchOne(
                "SELECT id FROM receipts WHERE csv_filename = ? AND riferimento_testo = ? AND riferimento_numero = ?",
                [$filename, $first['num_matricola'], $first['num_scontrino']]
            );
            if ($existing) continue;

            $customer = null;
            if (!empty($first['codice_cliente'])) {
                $customer = Database::fetchOne(
                    "SELECT id FROM customers WHERE contact_person = ? OR fiscal_code = ? OR cust_code = ?",
                    [$first['codice_cliente'], $first['codice_cliente'], $first['codice_cliente']]
                );
            }

            $subtotal = 0;
            $totalNonRiscosso = 0;
            foreach ($groupRows as $row) {
                $lineTotal = ($row['listino'] * $row['qta']) - $row['sconto_val'];
                $subtotal += $lineTotal;
                $totalNonRiscosso += $row['non_riscosso'];
            }
            $contestuale = round($subtotal - $totalNonRiscosso, 2);

            $receiptId = Database::insert('receipts', [
                'customer_id'         => $customer ? $customer['id'] : null,
                'riferimento_testo'   => $first['num_matricola'],
                'riferimento_numero'  => $first['num_scontrino'],
                'riferimento_data'    => $receiptDate,
                'store_code'          => $first['negozio'],
                'client_code_csv'     => $first['codice_cliente'],
                'subtotal'            => $subtotal,
                'total'               => $subtotal,
                'non_riscosso'        => $totalNonRiscosso,
                'advance_amount'      => $contestuale,
                'invoice_reason'      => 'S-VEND',
                'ei_document_type'    => 'TD24',
                'status'              => 'draft',
                'csv_filename'        => $filename,
                'created_at'          => date('Y-m-d H:i:s'),
                'updated_at'          => date('Y-m-d H:i:s'),
            ]);

            foreach ($groupRows as $lineNum => $row) {
                Database::insert('receipt_items', [
                    'receipt_id'       => $receiptId,
                    'line_number'      => $lineNum + 1,
                    'line_type'        => 'Merce',
                    'barcode'          => $row['barcode'],
                    'description'      => $row['descrizione'],
                    'uom'              => 'NR',
                    'quantity'         => $row['qta'],
                    'unit_value'       => $row['listino'],
                    'discount_formula' => $row['sconto_val'] > 0 ? number_format($row['sconto_val'], 2) : null,
                    'tax_rate'         => $row['codice_iva'],
                    'line_total'       => ($row['listino'] * $row['qta']) - $row['sconto_val'],
                ]);
            }

            $this->log('receipt_imported', 'receipts', $receiptId, [
                'file' => $filename,
                'lines' => count($groupRows),
            ]);
            $importedCount++;
        }

        Database::commit();
        return ['receipts' => $importedCount, 'rows' => count($rows)];
    }

}