
----------------------------------------------------
server/server.js

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

// ================================================================
// MIDDLEWARES
// ================================================================
app.set('trust proxy', true);
app.use(helmet({ contentSecurityPolicy: false }));
app.use(cors({ origin: true, credentials: true }));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Service des fichiers statiques (pour Angular)
app.use(express.static(path.join(__dirname, '../public')));

// Logger
app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
    next();
});

// ================================================================
// CHARGER LES CONFIGS
// ================================================================
let appConfigs = {};
try {
    appConfigs.countries = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/countries.json'), 'utf8'));
    appConfigs.currencies = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/currencies.json'), 'utf8'));
    appConfigs.taresRules = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/tares_codes.json'), 'utf8'));
		
    // [FLAG] Vérification de la configuration critique
    const openRouterApiKey = process.env.OPENROUTER_API_KEY;
    if (!openRouterApiKey || openRouterApiKey.length < 10) {
        console.warn(`[FLAG-INIT-WARN] OPENROUTER_API_KEY semble manquant ou trop court.`);
    } else {
        console.log(`[FLAG-INIT-OK] OPENROUTER_API_KEY chargée (${openRouterApiKey.substring(0, 10)}...).`);
    }
		
    // On attache les configs à l'objet 'app' pour qu'il soit accessible dans les routes
    app.appConfigs = appConfigs;
    console.log('✓ Fichiers de configuration chargés');
} catch (error) {
    console.error('⚠ Erreur chargement config:', error.message);
}

// ================================================================
// ROUTES DE L'API
// ================================================================
// AJOUT : Charger les routes depuis le fichier dédié
const apiRoutes = require('./routes/api');
app.use('/api', apiRoutes);

// ================================================================
// ROUTE FALLBACK POUR ANGULAR (Single Page Application)
// ================================================================
// Cette route doit être après les routes API
app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});

// ================================================================
// GESTION D'ERREURS
// ================================================================
app.use((err, req, res, next) => {
    console.error('Erreur:', err.stack);
    res.status(err.status || 500).json({
        error: process.env.NODE_ENV === 'production'
            ? 'Erreur interne du serveur'
            : err.message
    });
});

app.use((req, res) => {
    res.status(404).json({ error: 'Route API non trouvée: ' + req.path });
});

// ================================================================
// DÉMARRAGE
// ================================================================
app.listen(PORT, '127.0.0.1', () => {
    console.log(`╔════════════════════════════════════════════════════════════╗`);
    console.log(`║  Backend e-dec démarré sur http://127.0.0.1:${PORT}        ║`);
    console.log(`╚════════════════════════════════════════════════════════════╝`);
});

process.on('SIGTERM', () => {
    console.log('SIGTERM reçu, arrêt gracieux...');
    process.exit(0);
});
----------------------------------------------------
server/routes/api.js

const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { body, validationResult } = require('express-validator');
const { pool } = require('../db');
const ocrService = require('../services/ocrService');
const taresClassifier = require('../services/taresClassifier');
const edecGenerator = require('../services/edecGenerator');
const exchangeRateService = require('../services/exchangeRateService');
const { v4: uuidv4 } = require('uuid');

// Multer
const upload = multer({
  dest: path.join(__dirname, '../../public/uploads'),
  limits: { fileSize: 10 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'application/pdf'];
    if (allowed.includes(file.mimetype)) cb(null, true);
    else cb(new Error('Formats supportés: JPG, PNG, PDF'));
  }
});

// =========================================================================
// FONCTIONS DE RECHERCHE BRAND CODE (Optimisation : intégrées ici)
// =========================================================================

/**
 * Recherche le brand_code par le nom de la marque.
 */
async function getBrandCodeByBrandName(brandName) {
    if (!brandName) return null;
    const cleanedBrandName = brandName.trim().toUpperCase();
    const query = `SELECT brand_code FROM brand_codes WHERE brand_name = ?`;
    const [rows] = await pool.execute(query, [cleanedBrandName]);
    return rows.length > 0 ? rows[0].brand_code : null;
}

/**
 * Récupère la liste complète des brand_codes.
 */
async function getAllBrandCodes() {
    const query = `SELECT brand_code, brand_name FROM brand_codes ORDER BY brand_name`;
    const [rows] = await pool.execute(query);
    return rows;
}


// Health
router.get('/health', (req, res) => {
  res.json({
    status: 'OK',
    timestamp: new Date().toISOString(),
    service: 'e-dec Node.js Backend',
    version: '1.1.0'
  });
});

// Reference data
// CORRECTION : Utiliser req.app.appConfigs pour accéder aux configurations globales de l'application Express.
router.get('/countries', (req, res) => res.json(req.app.appConfigs.countries || {}));
router.get('/currencies', (req, res) => res.json(req.app.appConfigs.currencies || {}));

// Exchange rates
router.get('/exchange-rates', async (req, res) => {
  try {
    const rates = await exchangeRateService.getCurrentRates();
    res.json(rates);
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// Currency conversion
router.post('/convert-currency', [
  body('amount').isFloat({ min: 0 }),
  body('from').isString().isLength({ min: 3, max: 3 }),
  body('to').optional().isString().isLength({ min: 3, max: 3 })
], async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ error: 'Paramètres invalides', details: errors.array() });
    const { amount, from, to = 'CHF' } = req.body;
    const converted = await exchangeRateService.convert(Number(amount), from, to);
    res.json({ converted });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// =========================================================================
// NOUVELLE ROUTE : Brand Code Lookup
// =========================================================================
router.post('/brand-codes/lookup', [
    body('brand').trim().notEmpty().withMessage("Le nom de la marque est requis.")
], async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Champs invalides', details: errors.array() });
    }

    const { brand } = req.body;

    try {
        const brandCode = await getBrandCodeByBrandName(brand); // <-- Appel direct

        if (brandCode) {
            return res.json({ brandCode });
        }

        const options = await getAllBrandCodes(); // <-- Appel direct
        
        if (options.length === 0) {
            return res.status(404).json({ message: "Liste des codes de marque non disponible." });
        }

        return res.json({ options });

    } catch (error) {
        console.error('Erreur lors de la recherche du code de marque:', error);
        return res.status(500).json({ error: "Erreur interne du serveur lors de la recherche du code de marque." });
    }
});
// =========================================================================

// OCR
router.post('/ocr', upload.single('registration_card'), async (req, res) => {
  const filePath = req.file?.path;
  try {
    if (!filePath) return res.status(400).json({ error: 'Aucun fichier fourni' });
    const extractedData = await ocrService.extractFromFile(filePath, req.file.mimetype);
    res.json(extractedData);
  } catch (error) {
    console.error('Erreur OCR complète:', error);
    res.status(500).json({ 
      error: 'Erreur OCR', 
      details: error.message,
      hint: 'Vérifiez que votre clé API OpenRouter est valide et que le fichier est lisible'
    });
  } finally {
    if (filePath && fs.existsSync(filePath)) try { fs.unlinkSync(filePath); } catch (_) {}
  }
});

// Validate VIN + country
router.post('/validate-vin', [
  body('vin').isString().isLength({ min: 17, max: 17 })
], async (req, res) => {
  const { vin } = req.body;
  const valid = /^[A-HJ-NPR-Z0-9]{17}$/.test((vin || '').toUpperCase());
  let country = null;
  try {
    if (valid) {
      country = await taresClassifier.getCountryFromVIN_DB(vin);
    }
    res.json({ valid, country });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// Classification TARES
router.post('/classify-vehicle', async (req, res) => {
  try {
    // CORRECTION : Utiliser req.app.appConfigs pour accéder aux configurations globales.
    const classification = await taresClassifier.classify(req.body, req.app.appConfigs.taresRules);
    res.json(classification);
  } catch (error) {
    console.error('Erreur classification:', error);
    res.status(500).json({ error: error.message });
  }
});

// Generate Import XML + persist + Auto-Submit
// =========================================================================
// ROUTE MODIFIÉE : Le XML n'est plus renvoyé, mais la soumission est lancée en arrière-plan.
// =========================================================================
router.post('/generate-import', [
  body('user_name').isString().notEmpty(),
  body('user_zip').isString().notEmpty(),
  body('user_city').isString().notEmpty(),
  body('dispatch_country').isString().isLength({ min: 2, max: 2 }),
  body('transport_mode').isString().isIn(['9', '2', '3', '4', '7', '8']),
  body('purchase_price').isFloat({ min: 0 }),
  body('purchase_currency').isString().isLength({ min: 3, max: 3 }),
  body('vin').isString().isLength({ min: 17, max: 17 }),
  body('brand').isString().notEmpty(),
	body('brand_code').optional()
  .isString().isLength({ min: 3, max: 3 }).isNumeric().withMessage("Le brand_code doit être une chaîne de 3 chiffres uniquement (ex. : 123)."),
  body('model').isString().notEmpty(),
  body('weight_empty').isInt({ min: 1 }),
  body('weight_total').isInt({ min: 1 }),
], async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ error: 'Champs invalides', details: errors.array() });

  const conn = await pool.getConnection();
  const txId = uuidv4();
  
  try {
    await conn.beginTransaction();
    console.log(`[GENERATE-IMPORT] Début génération pour UUID: ${txId}`);

		// Calculer la somme totale (base pour la TVA)
		const sum = Number(req.body.purchase_price || 0) + Number(req.body.transport_cost || 0) + Number(req.body.other_costs || 0);

		// Convertir le prix d'achat SEUL en CHF pour la valeur statistique
		// La valeur statistique (statistical_value_chf) = purchase_price converti en CHF
		const purchasePriceOnly = Number(req.body.purchase_price || 0);
		const statistical_value_chf = await exchangeRateService.convert(purchasePriceOnly, req.body.purchase_currency, 'CHF');

		// Convertir la somme TOTALE (purchase_price + transport_cost + other_costs) en CHF pour la valeur TVA
		// La valeur TVA (vat_value_chf) = (vehicle cost + import costs) en CHF
		const vat_value_chf = await exchangeRateService.convert(sum, req.body.purchase_currency, 'CHF');

    // Determine TARES if not present
    let tar = req.body.commodity_code;
    let statKey = req.body.statistical_key;
    if (!tar || !statKey) {
      const cls = await taresClassifier.classify(req.body, req.app.appConfigs.taresRules);
      tar = tar || cls.commodity_code;
      statKey = statKey || cls.statistical_key || '911';
    }
		
		// LOGIQUE DE RÉCUPÉRATION DU BRAND CODE
		let finalBrandCode = req.body.brand_code;
		
		if (!finalBrandCode && req.body.brand) {
				// Tente la recherche automatique si le code n'a pas été fourni par le client
				const autoCode = await getBrandCodeByBrandName(req.body.brand); // <-- Appel direct
				finalBrandCode = autoCode;
		}

    // Origin from VIN
    let origin = await taresClassifier.getCountryFromVIN_DB(req.body.vin);
    origin = origin || req.body.dispatch_country || 'FR';

		// Persist declaration
		const [declRes] = await conn.execute(
			`INSERT INTO declarations (uuid, declaration_type, user_name, user_firstname, user_address, user_zip, user_city, user_country, user_ide, language, status) VALUES (?, 'IMPORT', ?, ?, ?, ?, ?, 'CH', ?, ?, 'generated')`,
			[
				txId,
				req.body.user_name || '',
				req.body.user_firstname || null,
				req.body.user_address || null,
				req.body.user_zip || '',
				req.body.user_city || '',
				req.body.user_ide || null,
				(req.body.language || 'fr').toLowerCase()
			]
		);
		
    const declarationId = declRes.insertId;
    console.log(`[GENERATE-IMPORT] Déclaration créée ID: ${declarationId}`);

		await conn.execute(
			`INSERT INTO import_details (declaration_id, dispatch_country, transport_mode, is_relocation, purchase_price, purchase_currency, transport_cost, other_costs, statistical_value, vat_value) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			[
				declarationId,
				req.body.dispatch_country,
				req.body.transport_mode,
				req.body.is_relocation ? 1 : 0,
				Number(req.body.purchase_price || 0),
				req.body.purchase_currency,
				Number(req.body.transport_cost || 0),
				Number(req.body.other_costs || 0),
				statistical_value_chf,
				vat_value_chf
			]
		);

		await conn.execute(
			`INSERT INTO vehicles (declaration_id, vin, brand, model, year, cylinder_capacity, fuel_type, weight_empty, weight_total, tares_code, statistical_key, country_origin, brand_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			[
				declarationId,
				req.body.vin.toUpperCase(),
				req.body.brand,
				req.body.model,
				req.body.year || null,
				req.body.cylinder_capacity || null,
				req.body.fuel_type || null,
				req.body.weight_empty,
				req.body.weight_total,
				tar,
				statKey,
				origin,
				finalBrandCode
			]
		);

    // Build XML (with all business rules)
    const xml = await edecGenerator.generateImport({
      ...req.body,
      commodity_code: tar,
      statistical_key: statKey,
      origin_country: origin,
      statistical_value_chf,
      vat_value_chf,
			brand_code: finalBrandCode
    });

    // Sauvegarder le XML
    const xmlDir = path.join(__dirname, '../../xml');
    if (!fs.existsSync(xmlDir)) {
      fs.mkdirSync(xmlDir, { recursive: true });
    }
    const xmlFilePath = path.join(xmlDir, `${txId}.xml`);
    fs.writeFileSync(xmlFilePath, xml, 'utf8');
    console.log(`[GENERATE-IMPORT] XML sauvegardé: ${xmlFilePath}`);

    await conn.execute(
			`UPDATE declarations SET xml_content = ?, xml_file_path = ?, status = 'generated' WHERE id = ?`,
			[xml, xmlFilePath, declarationId]
		);
    await conn.commit();

    // Lancer la soumission en arrière-plan sans attendre (timeout pour libérer la connexion DB de la route principale)
    console.log(`[GENERATE-IMPORT] Lancement soumission automatique pour: ${txId}`);
		
		setTimeout(async () => {
				let errorConn = null; // Déclaration pour la portée
				
				try {
						console.log(`[SUBMISSION-BACKGROUND] Début soumission automatique pour: ${txId}`);
						const edecSubmissionService = require('../services/edecSubmissionService');
						
						await edecSubmissionService.submitDeclaration(xmlFilePath, txId); 
						
						console.log(`[SUBMISSION-BACKGROUND] Soumission terminée avec succès pour: ${txId}`);
						
				} catch (error) {
						console.error(`[SUBMISSION-BACKGROUND] Erreur soumission pour ${txId}:`, error);
						
						// Gestion robuste de l'erreur de soumission
						try {
								errorConn = await pool.getConnection();
								
								// Vérifier d'abord si la colonne error_message existe
								const [columns] = await errorConn.execute(`
										SELECT COLUMN_NAME 
										FROM INFORMATION_SCHEMA.COLUMNS 
										WHERE TABLE_NAME = 'declarations' 
										AND COLUMN_NAME = 'error_message'
								`);
								
								const hasErrorMessageColumn = columns.length > 0;
								
								if (hasErrorMessageColumn) {
										await errorConn.execute(
												`UPDATE declarations SET status = 'submission_error', error_message = ? WHERE uuid = ?`, 
												[String(error.message).substring(0, 255), txId]
										);
								} else {
										// Fallback sans error_message
										await errorConn.execute(
												`UPDATE declarations SET status = 'submission_error' WHERE uuid = ?`, 
												[txId]
										);
										console.warn(`[SUBMISSION-BACKGROUND] Colonne error_message manquante pour ${txId}`);
								}
								
						} catch (dbError) {
								console.error(`[SUBMISSION-BACKGROUND] Erreur DB lors de la mise à jour du statut pour ${txId}:`, dbError);
								
								// Dernier fallback: essayer juste de logger l'erreur
								try {
										if (errorConn) {
												await errorConn.execute(
														`UPDATE declarations SET status = 'submission_error' WHERE uuid = ?`, 
														[txId]
												);
										}
								} catch (finalError) {
										console.error(`[SUBMISSION-BACKGROUND] Échec final pour ${txId}:`, finalError);
								}
						} finally {
								if (errorConn) {
										errorConn.release();
								}
						}
				}
		}, 100);

    // Répondre immédiatement avec l'UUID pour le suivi
    res.json({ 
      success: true, 
      sessionToken: txId,
      message: 'Déclaration générée et soumission automatique démarrée'
    });

  } catch (e) {
    await conn.rollback();
    console.error('[GENERATE-IMPORT] Erreur:', e);
    res.status(500).json({ error: e.message });
  } finally {
    conn.release();
  }
});

// =========================================================================
// NOUVELLE ROUTE : Statut de soumission
// =========================================================================
router.get('/submission-status/:sessionToken', async (req, res) => {
  const { sessionToken } = req.params;
  
  try {
    const [rows] = await pool.execute(`
      SELECT status, declaration_number, liste_importation_path, bulletin_delivrance_path, 
             COALESCE(error_message, '') as error_message
      FROM declarations 
      WHERE uuid = ?
    `, [sessionToken]);
    
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Session non trouvée' });
    }
    
    const declaration = rows[0];
    
    // Déterminer la progression
    let progress = 0;
    let step = '';
    
    switch (declaration.status) {
      case 'generated':
        progress = 25;
        step = 'Déclaration générée';
        break;
      case 'submitting':
        progress = 50;
        step = 'Connexion au portail e-dec';
        break;
      case 'uploading':
        progress = 60;
        step = 'Upload du fichier XML';
        break;
      case 'processing':
        progress = 75;
        step = 'Traitement par la douane';
        break;
      case 'submitted':
        progress = 100;
        step = 'Soumission terminée';
        break;
      case 'submission_error':
        progress = 0;
        step = 'Erreur de soumission';
        break;
      default:
        progress = 10;
        step = 'Initialisation';
    }
    
    res.json({
      status: declaration.status,
      progress,
      step,
      declarationNumber: declaration.declaration_number,
      listePath: declaration.liste_importation_path,
      bulletinPath: declaration.bulletin_delivrance_path,
      error: declaration.error_message
    });
    
  } catch (error) {
    console.error('[SUBMISSION-STATUS] Erreur:', error);
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// =========================================================================
// NOUVELLE ROUTE : Téléchargement PDF
// =========================================================================
router.get('/download-pdf', (req, res) => {
  const filePath = req.query.path;
  
  if (!filePath || !fs.existsSync(filePath)) {
    return res.status(404).json({ error: 'Fichier non trouvé' });
  }
  
  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
  
  const fileStream = fs.createReadStream(filePath);
  fileStream.pipe(res);
});

// Generate Export XML + persist
router.post('/generate-export', [
  body('user_name').isString().notEmpty(),
  body('user_zip').isString().notEmpty(),
  body('user_city').isString().notEmpty(),
  body('destination_country').isString().isLength({ min: 2, max: 2 }),
  body('vin').isString().isLength({ min: 17, max: 17 }),
  body('brand').isString().notEmpty(),
  body('model').isString().notEmpty(),
], async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ error: 'Champs invalides', details: errors.array() });

  const conn = await pool.getConnection();
  const txId = uuidv4(); // Ajout de txId pour l'export, même logique d'UUID
  try {
    await conn.beginTransaction();

    const [declRes] = await conn.execute(`
      INSERT INTO declarations (uuid, declaration_type, user_name, user_firstname, user_address, user_zip, user_city, user_country, user_ide, language, status)
      VALUES (?, 'EXPORT', ?, ?, ?, ?, ?, 'CH', ?, ?, 'draft')
    `, [
      txId,
      req.body.user_name || '',
      req.body.user_firstname || null,
      req.body.user_address || null,
      req.body.user_zip || '',
      req.body.user_city || '',
      req.body.user_ide || null,
      (req.body.language || 'fr').toLowerCase()
    ]);
    const declarationId = declRes.insertId;

    await conn.execute(`
      INSERT INTO export_details (declaration_id, destination_country, taxation_code)
      VALUES (?, ?, ?)
    `, [
      declarationId,
      req.body.destination_country,
      req.body.taxation_code || null
    ]);

    await conn.execute(`
      INSERT INTO vehicles (declaration_id, vin, brand, model, year, cylinder_capacity, fuel_type, weight_empty, weight_total, tares_code, statistical_key, country_origin, brand_code)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
      declarationId,
      req.body.vin.toUpperCase(),
      req.body.brand,
      req.body.model,
      req.body.year || null,
      req.body.cylinder_capacity || null,
      req.body.fuel_type || null,
      req.body.weight_empty || null,
      req.body.weight_total || null,
      req.body.commodity_code || '8703.9060',
      req.body.statistical_key || '911',
      req.body.origin_country || null,
      req.body.brand_code || null
    ]);

    // Build XML
    const xml = await edecGenerator.generateExport(req.body);

    // Sauvegarder le XML (ajout de la sauvegarde de fichier pour la cohérence avec l'import)
    const xmlDir = path.join(__dirname, '../../xml');
    if (!fs.existsSync(xmlDir)) {
      fs.mkdirSync(xmlDir, { recursive: true });
    }
    const xmlFilePath = path.join(xmlDir, `${txId}.xml`);
    fs.writeFileSync(xmlFilePath, xml, 'utf8');

    await conn.execute(`UPDATE declarations SET xml_content = ?, xml_file_path = ?, status = 'generated' WHERE id = ?`, [xml, xmlFilePath, declarationId]);

    await conn.commit();

    res.set('Content-Type', 'application/xml');
    res.set('Content-Disposition', `attachment; filename="edec-export-${Date.now()}.xml"`);
    res.send(xml);
  } catch (e) {
    await conn.rollback();
    console.error('Erreur génération export:', e);
    res.status(500).json({ error: e.message });
  } finally {
    conn.release();
  }
});


module.exports = router;
----------------------------------------------------
server/services/edecGenerator.js

const edecGeneratorImport = require('./edecGeneratorImport');
const edecGeneratorExport = require('./edecGeneratorExport');

class EdecGenerator {
  generateImport(data) {
    return edecGeneratorImport.generateImport(data);
  }
  generateExport(data) {
    return edecGeneratorExport.generateExport(data);
  }
}

module.exports = new EdecGenerator();
----------------------------------------------------
server/services/edecSubmissionService.js

const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const { pool } = require('../db');

class EdecSubmissionService {
    constructor() {
        this.baseUrl = 'https://e-dec-web.ezv.admin.ch/webdec/main.xhtml';
        this.downloadDir = path.join(__dirname, '../../edecpdf');

        if (!fs.existsSync(this.downloadDir)) {
            fs.mkdirSync(this.downloadDir, { recursive: true });
        }
    }

    async updateStatus(sessionToken, status) {
        const conn = await pool.getConnection();
        try {
            console.log(`[STATUS-UPDATE] ${sessionToken} -> ${status}`);
            const errorMessage = status.includes('error') ? null : '';
            await conn.execute(
                `UPDATE declarations SET status = ?, error_message = ? WHERE uuid = ?`,
                [status, errorMessage, sessionToken]
            );
        } catch (error) {
            console.error(`[STATUS-UPDATE-ERROR] ${sessionToken}:`, error);
            throw error;
        } finally {
            conn.release();
        }
    }

    async submitDeclaration(xmlFilePath, sessionToken) {
        let browser = null;

        try {
            console.log(`[EDEC Submit] Démarrage soumission pour token: ${sessionToken}`);

            try {
                await this.updateStatus(sessionToken, 'submitting');
            } catch (statusError) {
                console.warn(`[EDEC Submit] Warning update status: ${statusError.message}`);
            }

            browser = await chromium.launch({
                headless: true,
                args: [
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-dev-shm-usage',
                    '--disable-gpu',
                    '--disable-software-rasterizer',
                    '--disable-extensions'
                ]
            });

            const context = await browser.newContext({
                acceptDownloads: true,
                userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                viewport: { width: 1280, height: 720 }
            });

            const page = await context.newPage();

            page.on('console', msg => console.log('[BROWSER]', msg.text()));
            page.on('pageerror', error => console.log('[BROWSER-ERROR]', error));

            console.log('[EDEC Submit] Navigation vers le portail...');
            await page.goto(this.baseUrl, {
                waitUntil: 'networkidle',
                timeout: 60000
            });

            await page.waitForTimeout(2000);

            console.log('[EDEC Submit] Clic sur "Zollanmeldung laden"...');
            await page.click('#mainform\\:loadDeclaration');
            await page.waitForTimeout(2000);

            console.log('[EDEC Submit] Attente de la popup de chargement...');
            await page.waitForSelector('#mainform\\:declarationLoadPopup', { 
                state: 'visible',
                timeout: 10000 
            });

            console.log('[EDEC Submit] Recherche de l\'iframe d\'upload...');
            const iframeElement = await page.waitForSelector(
                'iframe#mainform\\:inputFileComponent\\:uploadFrame',
                { state: 'attached', timeout: 10000 }
            );

            const frame = await iframeElement.contentFrame();
            if (!frame) {
                throw new Error('Impossible d\'accéder au contenu de l\'iframe');
            }

            console.log('[EDEC Submit] Upload du fichier XML...');
            const fileInput = await frame.waitForSelector('input[type="file"]', {
                state: 'attached',
                timeout: 10000
            });

            await fileInput.setInputFiles(xmlFilePath);
            await page.waitForTimeout(2000);

            console.log('[EDEC Submit] Clic sur le bouton OK...');
            await page.click('a#mainform\\:confirmButton');
            
            console.log('[EDEC Submit] Fichier chargé, attente traitement...');

            try {
                await this.updateStatus(sessionToken, 'processing');
            } catch (statusError) {
                console.warn(`[EDEC Submit] Warning update status: ${statusError.message}`);
            }

            await page.waitForTimeout(5000);

            console.log('[EDEC Submit] Recherche du bouton Senden dans le menu de navigation...');
            
            const sendButton = await page.waitForSelector('a#j_id49\\:j_id69', {
                state: 'visible',
                timeout: 15000
            });

            console.log('[EDEC Submit] Clic sur Senden pour envoyer la déclaration...');
            await sendButton.click();
            await page.waitForTimeout(8000);

            // Extraction du numéro de déclaration avec la méthode optimisée
            let declarationNumber = null;
            
            console.log('[EDEC Submit] Extraction du numéro de déclaration...');
            
            try {
                // Attendre le message de confirmation avec le span iceOutFrmt
                const confirmationSpan = await page.waitForSelector('span.iceOutFrmt', { 
                    timeout: 15000 
                });

                if (confirmationSpan) {
                    const spanText = await confirmationSpan.textContent();
                    console.log(`[EDEC Submit] Texte du span trouvé: ${spanText}`);
                    
                    // Extraction du numéro qui commence par l'année (25CHWI, 26CHWI, etc.)
                    const currentYear = new Date().getFullYear().toString().slice(-2);
                    const match = spanText.match(new RegExp(`(${currentYear}CHWI\\d{15})`));
                    
                    if (match) {
                        declarationNumber = match[1];
                        console.log(`[EDEC Submit] Numéro trouvé: ${declarationNumber}`);
                    } else {
                        // Fallback: chercher n'importe quel pattern XXCHWI suivi de 15 chiffres
                        const fallbackMatch = spanText.match(/(\d{2}CHWI\d{15})/);
                        if (fallbackMatch) {
                            declarationNumber = fallbackMatch[1];
                            console.log(`[EDEC Submit] Numéro trouvé (fallback): ${declarationNumber}`);
                        }
                    }
                }

                if (!declarationNumber) {
                    console.warn('[EDEC Submit] Numéro de déclaration non trouvé dans le span, mais on continue pour les PDF...');
                }
            } catch (numError) {
                console.warn(`[EDEC Submit] Erreur extraction numéro (non bloquant): ${numError.message}`);
            }

            // Téléchargement des PDF
            console.log('[EDEC Submit] Recherche des liens PDF...');
            
            await page.waitForSelector('a[href*=".pdf"]', { timeout: 15000 });
            
            const pdfLinks = await page.$$('a.downloadLink[href*=".pdf"]');
            
            if (pdfLinks.length < 2) {
                throw new Error(`Seulement ${pdfLinks.length} PDF trouvé(s), 2 attendus`);
            }

            console.log(`[EDEC Submit] ${pdfLinks.length} liens PDF trouvés`);

            const listePath = path.join(this.downloadDir, `${sessionToken}_liste_importation.pdf`);
            const bulletinPath = path.join(this.downloadDir, `${sessionToken}_bulletin_delivrance.pdf`);

            console.log('[EDEC Submit] Téléchargement 1er PDF...');
            const downloadPromise1 = page.waitForEvent('download', { timeout: 30000 });
            await pdfLinks[0].click();
            const download1 = await downloadPromise1;
            await download1.saveAs(listePath);
            console.log(`[EDEC Submit] PDF 1 sauvegardé: ${listePath}`);

            console.log('[EDEC Submit] Téléchargement 2ème PDF...');
            await page.waitForTimeout(1000);
            const downloadPromise2 = page.waitForEvent('download', { timeout: 30000 });
            await pdfLinks[1].click();
            const download2 = await downloadPromise2;
            await download2.saveAs(bulletinPath);
            console.log(`[EDEC Submit] PDF 2 sauvegardé: ${bulletinPath}`);

            console.log('[EDEC Submit] Mise à jour base de données...');
            await this.saveToDatabase(sessionToken, declarationNumber || 'UNKNOWN', listePath, bulletinPath);

            console.log('[EDEC Submit] ✓ Soumission terminée avec succès');
            return {
                success: true,
                declarationNumber: declarationNumber || 'UNKNOWN',
                listePath,
                bulletinPath
            };

        } catch (error) {
            console.error('[EDEC Submit] ✗ Erreur:', error);

            try {
                const errorConn = await pool.getConnection();
                try {
                    const errorMessage = String(error.message).substring(0, 255);
                    await errorConn.execute(
                        `UPDATE declarations SET status = 'submission_error', error_message = ? WHERE uuid = ?`,
                        [errorMessage, sessionToken]
                    );
                } finally {
                    errorConn.release();
                }
            } catch (dbError) {
                console.error('[EDEC Submit] Erreur DB:', dbError);
            }

            throw error;
        } finally {
            if (browser) {
                await browser.close();
            }
        }
    }

    async saveToDatabase(sessionToken, declarationNumber, listePath, bulletinPath) {
        const conn = await pool.getConnection();
        try {
            await conn.execute(`
                UPDATE declarations 
                SET 
                    declaration_number = ?,
                    liste_importation_path = ?,
                    bulletin_delivrance_path = ?,
                    status = 'submitted',
                    submitted_at = NOW()
                WHERE uuid = ?
            `, [declarationNumber, listePath, bulletinPath, sessionToken]);

        } finally {
            conn.release();
        }
    }
}

module.exports = new EdecSubmissionService();
----------------------------------------------------
server/services/edecGeneratorImport.js

function escapeXml(value) {
  if (value === null || value === undefined) return '';
  return String(value)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

// invoiceCurrencyType mapping:
// 1=CHF, 2=EUR, 3=USD, 4=Autre monnaie UE, 5=Autre monnaie
function mapInvoiceCurrencyType(currency) {
  if (currency === 'CHF') return 1;
  if (currency === 'EUR') return 2;
  if (currency === 'USD') return 3;

  // Liste des pays de l'UE qui n'utilisent pas l'Euro (BGN, CZK, DKK, HUF, PLN, RON, SEK)
  const euNonEuroCurrencies = ['BGN', 'CZK', 'DKK', 'HUF', 'PLN', 'RON', 'SEK'];
  if (euNonEuroCurrencies.includes(currency)) {
    return 4; // Autre monnaie UE
  }

  return 5; // Autre monnaie
}

class EdecGeneratorImport {
  generateImport(data) {
    // CORRECTION: Remplace 'T' par un espace et 'Z' par ' UTC' pour le format e-dec (YYYY-MM-DD HH:mm:ss.sss UTC)
    const now = new Date().toISOString().replace('T', ' ').replace('Z', ' UTC');
    
    const lang = (data.language || 'fr').toLowerCase();
    const dispatchCountry = data.dispatch_country || 'FR';
    const transportMode = data.transport_mode || '9'; 
    
    // Le nom (raison sociale ou Prénom Nom) est désormais formaté par le controller client
    const importerName = escapeXml(data.user_name || '');
    const importerStreet = escapeXml(data.user_address || '');
    const importerZip = escapeXml(data.user_zip || '');
    const importerCity = escapeXml(data.user_city || '');
    const importerCountry = 'CH';
    const ide = escapeXml(data.user_ide || 'CHE222251936'); // Particulier par défaut

    const consigneeName = escapeXml(data.consignee_name || data.user_name || '');
    const consigneeStreet = escapeXml(data.consignee_address || data.user_address || '');
    const consigneeZip = escapeXml(data.consignee_zip || data.user_zip || '');
    const consigneeCity = escapeXml(data.consignee_city || data.user_city || '');
    const consigneeIde = escapeXml(data.consignee_ide || ide);

    const declarantName = escapeXml(data.declarant_name || data.user_name || '');
    const declarantStreet = escapeXml(data.declarant_address || data.user_address || '');
    const declarantZip = escapeXml(data.declarant_zip || data.user_zip || '');
    const declarantCity = escapeXml(data.declarant_city || data.user_city || '');
    const declarantIde = escapeXml(data.declarant_ide || '');

    const description = escapeXml(`${data.brand || ''} ${data.model || ''} ${data.year || ''}`.trim());
    const commodityCode = escapeXml(data.commodity_code || '8703.9060'); 
    const statisticalCode = escapeXml(data.statistical_key || '911'); 
    
    // Les masses doivent être des nombres entiers (arrondi par précaution)
    const grossMass = Math.round(Number(data.weight_total || 0));
    const netMass = Math.round(Number(data.weight_empty || 0));

    // Correction: La valeur statistique et la base TVA sont arrondies à l'ENTIER.
    // statisticalValue (Prix d'achat hors coûts annexes)
    const statisticalValue = Math.round(Number(data.statistical_value_chf || data.statistical_value || 0)); 
    // vatValue (Prix d'achat + coûts annexes)
    const vatValue = Math.round(Number(data.vat_value_chf || data.vatValue || statisticalValue));

    const vin = escapeXml(data.vin || '');
    const brandCode = escapeXml(data.brand_code || '');

    const originCountry = escapeXml(data.origin_country || data.originCountry || dispatchCountry || 'FR');
    const preference = 0; // pas de tarif préférentiel par défaut

    // TVA code: 1 normal, 3 déménagement
    const vatCode = data.is_relocation ? '3' : '1';

    // Additional tax (IVA) RG 660: key 001 (assujetti) ou 002 (exonéré)
    const additionalTaxKey = data.is_iva_exempt ? '002' : '001';
    // Assure que la quantité pour la taxe additionnelle est un entier
    const additionalTaxQty = Math.round(Number(data.additional_tax_value || data.purchase_price || statisticalValue || 0));

    // Utilisation de data.purchase_currency pour déduire le type de monnaie
    const invoiceCurrencyType = mapInvoiceCurrencyType(data.purchase_currency);
    
    // Construction des détails de l'article de marchandise (VIN et Brand Code)
    let goodsItemDetailsXml = '';
    if (vin || brandCode) {
        goodsItemDetailsXml = `
        <goodsItemDetails>
          <goodsItemDetail>
            <GoodsItemDetail>
              <name>2</name>
              <value>${vin}</value>
            </GoodsItemDetail>
            ${brandCode ? `
            <GoodsItemDetail>
              <name>1</name>
              <value>${brandCode}</value>
            </GoodsItemDetail>` : ''}
          </goodsItemDetail>
        </goodsItemDetails>`;
    }

    // Ajout de l'entête XML et utilisation de la variable now corrigée
    const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0" createdDate="${now}">
  <goodsDeclarationType>
    <serviceType>1</serviceType>
    <declarationType>1</declarationType>
    <language>${lang}</language>
    <dispatchCountry>${dispatchCountry}</dispatchCountry>
    <transportMeans>
      <transportMode>${transportMode}</transportMode>
      <transportationNumber></transportationNumber>
    </transportMeans>
    <transportInContainer>0</transportInContainer>
    <previousDocument/>
    <importer>
      <name>${importerName}</name>
      ${importerStreet ? `<street>${importerStreet}</street>` : ''}
      <postalCode>${importerZip}</postalCode>
      <city>${importerCity}</city>
      <country>${importerCountry}</country>
      <traderIdentificationNumber>${ide}</traderIdentificationNumber>
    </importer>
    <consignee>
      <name>${consigneeName}</name>
      ${consigneeStreet ? `<street>${consigneeStreet}</street>` : ''}
      <postalCode>${consigneeZip}</postalCode>
      <city>${consigneeCity}</city>
      <country>${importerCountry}</country>
      <traderIdentificationNumber>${consigneeIde}</traderIdentificationNumber>
    </consignee>
    <declarant>
      ${declarantIde ? `<traderIdentificationNumber>${declarantIde}</traderIdentificationNumber>` : '<traderIdentificationNumber></traderIdentificationNumber>'}
      <name>${declarantName}</name>
      ${declarantStreet ? `<street>${declarantStreet}</street>` : ''}
      <postalCode>${declarantZip}</postalCode>
      <city>${declarantCity}</city>
      <country>${importerCountry}</country>
    </declarant>
    <business>
      <customsAccount>0</customsAccount>
      <vatAccount>0</vatAccount>
      <vatSuffix>0</vatSuffix>
      <invoiceCurrencyType>${invoiceCurrencyType}</invoiceCurrencyType>
    </business>
    <goodsItem>
      <GoodsItemType>
        <traderItemID>0</traderItemID>
        <description>${description}</description>
        <commodityCode>${commodityCode}</commodityCode>
        <statisticalCode>${statisticalCode}</statisticalCode>
        <grossMass>${grossMass}</grossMass>
        <netMass>${netMass}</netMass>
        <additionalUnit>1</additionalUnit>
        <permitObligation>0</permitObligation>
        <nonCustomsLawObligation>2</nonCustomsLawObligation>
        <statistic>
          <customsClearanceType>1</customsClearanceType>
          <commercialGood>1</commercialGood>
          <statisticalValue>${statisticalValue}</statisticalValue>
          <repair>0</repair>
        </statistic>
        <origin>
          <originCountry>${originCountry}</originCountry>
          <preference>${preference}</preference>
        </origin>
        ${goodsItemDetailsXml}
        <packaging>
          <PackagingType>
            <packagingType>VN</packagingType>
            <quantity>1</quantity>
            <packagingReferenceNumber>${description}</packagingReferenceNumber>
          </PackagingType>
        </packaging>
        <valuation>
          <netDuty>0</netDuty>
          <vatValue>${vatValue}</vatValue>
          <vatCode>${vatCode}</vatCode>
        </valuation>
        <additionalTax>
          <AdditionalTaxType>
            <type>660</type>
            <key>${additionalTaxKey}</key>
            <quantity>${additionalTaxQty}</quantity>
          </AdditionalTaxType>
        </additionalTax>
      </GoodsItemType>
    </goodsItem>
  </goodsDeclarationType>
</EdecWeb>`;

    return xml;
  }
}

module.exports = new EdecGeneratorImport();
----------------------------------------------------
server/services/ocrService.js

const axios = require('axios');
const fs = require('fs');
const sharp = require('sharp');
const path = require('path');

/**
 * Service pour l'OCR (Reconnaissance Optique de Caractères)
 * utilisant l'API OpenRouter (Gemini) pour extraire les données
 * de documents (images ou PDF) de type carte grise/certificat d'immatriculation.
 */
class OcrService {
    constructor() {
        // Initialisation des propriétés de configuration
        this.apiKey = process.env.OPENROUTER_API_KEY;
        this.model = process.env.OPENROUTER_MODEL || 'google/gemini-2.5-flash';
        
        console.log('[OCR Service] Initialisé avec:', {
            hasApiKey: !!this.apiKey,
            apiKeyPreview: this.apiKey ? this.apiKey.substring(0, 10) + '...' : 'MANQUANT',
            model: this.model
        });
    }

    /**
     * Construit le prompt détaillé pour l'extraction de données de carte grise.
     * @returns {string} Le prompt à envoyer au modèle.
     */
    buildExtractionPrompt() {
        return `Tu es un expert en extraction de données de certificats d'immatriculation (cartes grises) européens (Suisse, France, Allemagne, Italie, etc.). 
		Analyse l'image ou le document fourni et retourne UNIQUEMENT un objet JSON strict correspondant au format: 
			{"vin":"VF3LCYHZPFS123456","brand":"Peugeot","model":"308","year":2015,"cylinder_capacity":1560,"fuel_type":"diesel","weight_empty":1245,"weight_total":1870,"type_approval":"e11*2007/46*0009*15"} 
		RÈGLES ET TERMES DE RECHERCHE PAR CHAMP: 
			- vin: 17 caractères alphanumériques. Cherche les champs E, 23, FIN, Chassis N., Fahrgestell-Nr, Telaio n., ou VIN. 
				Supprime les espaces internes pour la valeur finale. 
				IMPORTANT: Les lettres I, O et Q ne sont JAMAIS utilisées dans un VIN (norme ISO 3779).
				Si tu rencontres un I, considère-le comme un 1. Si tu rencontres un O ou un Q, considère-les comme des 0. 
			- brand: D.1, Marke.
			- model: D.3, Modèle, Typ. 
			- year: L'année de la première mise en circulation.
			- Cherche les champs B, 1. Inverkehrsetzung, 1ère mise en circulation, Data di prima immatricolazione. Isole UNIQUEMENT l'année (YYYY). 
			- cylinder_capacity: P.1, Hubraum, Cilindrata, Cylindrée. (Int en cm³) 
			- fuel_type: P.3, type de carburant, Carburant, Kraftstoff, Alimentazione. (essence|diesel|electrique|hybride|hybride_plugin) 
			- weight_empty: G, Leergewicht, Poids à vide, massa a vuoto. (Int en kg) 
			- weight_total: F.2, Gesamtgewicht, Poids total autorisé en charge, Massa massima ammissibile a pieno carico. (Int en kg) 
			- type_approval: K, Réception par type, Typengenehmigung, Approvazione del tipo. 
			Retourne strictement du JSON valide sans commentaire ni texte. Si une valeur est totalement inconnue, utilise: null.`.trim();
    }

    /**
     * Valide et nettoie un numéro VIN.
     * @param {string} vin - Le VIN à valider.
     * @returns {string|null} Le VIN nettoyé ou null si invalide.
     */
    validateVIN(vin) {
        if (!vin) return null;

        // Supprimer tous les espaces et normaliser en majuscules
        let clean = vin.replace(/\s/g, '').toUpperCase().trim();

        // Correction automatique des lettres interdites (ISO 3779: I, O, Q)
        const corrected = clean
            .replace(/I/g, '1')
            .replace(/[OQ]/g, '0');

        if (clean !== corrected) {
            console.warn('[OCR] VIN corrigé automatiquement:', {
                original: vin,
                corrected
            });
        }

        // Vérifie que c'est bien un VIN valide (17 caractères, sans I, O, Q)
        return /^[A-HJ-NPR-Z0-9]{17}$/.test(corrected) ? corrected : null;
    }

    /**
     * Valide l'année.
     * @param {number|string} year - L'année à valider.
     * @returns {number|null} L'année ou null.
     */
    validateYear(year) {
        if (!year) return null;
        const y = parseInt(year, 10);
        return (y >= 1900 && y <= new Date().getFullYear() + 1) ? y : null;
    }

    /**
     * Valide un entier positif.
     * @param {number|string} value - La valeur à valider.
     * @returns {number|null} L'entier ou null.
     */
    validateInteger(value) {
        if (value === null || value === undefined || value === '') return null;
        const int = parseInt(value, 10);
        // Vérifie que c'est un nombre fini et strictement positif
        return Number.isFinite(int) && int > 0 ? int : null;
    }

    /**
     * Normalise le type de carburant.
     * @param {string} fuelType - Le type de carburant brut.
     * @returns {string|null} Le type normalisé ou null.
     */
    normalizeFuelType(fuelType) {
        if (!fuelType) return null;
        const normalized = fuelType.toLowerCase().trim();
        if (normalized.includes('plug')) return 'hybride_plugin';
        if (normalized.includes('hybr')) return 'hybride';
        if (normalized.includes('elec') || normalized.includes('élec')) return 'electrique';
        if (normalized.includes('dies')) return 'diesel';
        if (normalized.includes('ess') || normalized.includes('petrol') || normalized.includes('gasoline')) return 'essence';
        return normalized;
    }

    /**
     * Nettoie et parse la réponse JSON du modèle.
     * @param {string} response - La réponse brute du modèle.
     * @returns {object} L'objet de données parsé et validé.
     */
    parseGeminiJSON(response) {
        console.log('[OCR] ÉTAPE 7a - Parsing JSON, réponse brute:', response);
        // Supprime les balises de code Markdown JSON
        const clean = response.replace(/```json|```/g, '').trim();
        console.log('[OCR] ÉTAPE 7b - Après nettoyage:', clean);

        try {
            const data = JSON.parse(clean);
            console.log('[OCR] ÉTAPE 7c - JSON parsé:', data);

            // Validation et normalisation des champs extraits
            return {
                vin: this.validateVIN(data.vin),
                brand: data.brand || null,
                model: data.model || null,
                year: this.validateYear(data.year),
                cylinder_capacity: this.validateInteger(data.cylinder_capacity),
                fuel_type: this.normalizeFuelType(data.fuel_type),
                weight_empty: this.validateInteger(data.weight_empty),
                weight_total: this.validateInteger(data.weight_total),
                type_approval: data.type_approval || null
            };
        } catch (err) {
            console.error('[OCR] ERREUR ÉTAPE 7 - Parsing JSON:', err);
            throw new Error('Format de réponse OCR invalide: ' + err.message);
        }
    }

    /**
     * Extrait les données d'un fichier (image ou PDF) via l'API OpenRouter.
     * @param {string} filePath - Chemin d'accès au fichier.
     * @param {string} mimeType - Type MIME du fichier.
     * @returns {Promise<object>} Les données extraites.
     */
    async extractFromFile(filePath, mimeType) {
        console.log('[OCR] ÉTAPE 1 - Début extraction:', { filePath, mimeType });

        if (!this.apiKey) {
            throw new Error('OPENROUTER_API_KEY manquante dans .env');
        }

        // Validation simple du type MIME
        const safeMime = ['image/jpeg', 'image/png', 'application/pdf'].includes(mimeType) ? mimeType : 'image/jpeg';
        console.log('[OCR] ÉTAPE 2 - Type MIME validé:', safeMime);

        let base64Data;
        let contentType;
        let dataUrlPreview;

        try {
            if (safeMime === 'application/pdf') {
                console.log('[OCR] ÉTAPE 3a - Traitement PDF');
                const pdfBuffer = fs.readFileSync(filePath);
                console.log('[OCR] PDF lu, taille:', pdfBuffer.length, 'bytes');

                // Conversion PDF en Base64
                base64Data = Buffer.from(pdfBuffer).toString('base64');
                contentType = 'application/pdf';
                console.log('[OCR] PDF converti en base64, longueur:', base64Data.length);
            } else {
                // Traitement d'image (optimisation avec sharp)
                console.log('[OCR] ÉTAPE 3b - Traitement Image');
                const optimized = await sharp(filePath)
                    .rotate()
                    .resize({ width: 1800, withoutEnlargement: true })
                    .jpeg({ quality: 80 })
                    .toBuffer();
                console.log('[OCR] Image optimisée, taille:', optimized.length, 'bytes');
                base64Data = optimized.toString('base64');
                contentType = 'image/jpeg';
                console.log('[OCR] Image convertie en base64, longueur:', base64Data.length);
            }

            dataUrlPreview = `data:${contentType};base64,...${base64Data.substring(base64Data.length - 10)}`;
            console.log('[OCR] ÉTAPE 3c - Data URL construite, aperçu:', dataUrlPreview);

            const dataSizeMB = base64Data.length * 0.75 / (1024 * 1024);
            console.log(`[OCR] ÉTAPE 3d - Taille du contenu à envoyer: ${dataSizeMB.toFixed(2)} MB`);
            if (dataSizeMB > 15) {
                console.warn('[OCR] AVERTISSEMENT: La taille du contenu est supérieure à 15MB, risque d\'échec API.');
            }

        } catch (error) {
            console.error('[OCR] ERREUR ÉTAPE 3 - Lecture/conversion fichier:', error);
            throw new Error('Impossible de lire le fichier: ' + error.message);
        }

        // Début de la construction de la requête API (Étapes 4 et 5 fusionnées)
        const prompt = this.buildExtractionPrompt();
        console.log('[OCR] ÉTAPE 4 - Prompt construit, longueur:', prompt.length);
        console.log('[OCR] ÉTAPE 5 - Envoi requête à OpenRouter');

        const fileContent = {};
        const dataUrl = `data:${contentType};base64,${base64Data}`;

        // Construction dynamique de l'objet pour le contenu multimédia (image_url ou file)
        if (contentType === 'application/pdf') {
            // Utilisation du type 'file' pour les PDFs
            fileContent.type = 'file';
            fileContent.file = {
                file_data: dataUrl,
                filename: path.basename(filePath)
            };
        } else {
            // Utilisation du type 'image_url' pour les images
            fileContent.type = 'image_url';
            fileContent.image_url = {
                url: dataUrl
            };
        }

        try {
            const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
                model: this.model,
                messages: [
                    {
                        role: 'user',
                        content: [
                            { type: 'text', text: prompt },
                            fileContent // L'objet construit (image_url ou file)
                        ]
                    }
                ],
                // Paramètres spécifiques pour l'extraction de données
                temperature: 0.1, 
                max_tokens: 4000 // Remis à 4000 (comme dans le 1er fragment) pour plus de flexibilité
            }, {
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`,
                    'Content-Type': 'application/json',
                    // Utilisez une logique de fallback pour le referer
                    'HTTP-Referer': process.env.REFERER_URL || 'http://localhost:3000', 
                    'X-Title': 'e-dec Vehicles' // Maintenu le titre spécifique
                },
                timeout: 30000
            });

            console.log('[OCR] ÉTAPE 6 - Réponse reçue:', {
                status: response.status,
                hasChoices: !!response.data?.choices,
                choicesLength: response.data?.choices?.length
            });

            const extractedText = response.data.choices?.[0]?.message?.content || '';
            console.log('[OCR] ÉTAPE 7 - Contenu extrait (aperçu):', extractedText.substring(0, 200));

            // Parsing et validation
            const parsed = this.parseGeminiJSON(extractedText);
            console.log('[OCR] ÉTAPE 8 - JSON parsé et validé avec succès:', parsed);

            return parsed;

        } catch (error) {
            // Gestion détaillée des erreurs API (étapes 5/6)
            const status = error.response?.status || 'Aucun';
            const code = error.code || 'Inconnu';

            console.error('[OCR] ERREUR ÉTAPE 5/6 - Requête API:', {
                message: error.message,
                status: status,
                statusText: error.response?.statusText,
                data: error.response?.data
            });

            if (status === 401) {
                throw new Error('Clé API OpenRouter invalide ou expirée (401)');
            }
            if (status === 429) {
                throw new Error('Quota OpenRouter dépassé (429)');
            }
            if (code === 'ECONNABORTED') {
                throw new Error('Timeout: OpenRouter n\'a pas répondu dans les 30 secondes');
            }

            // Erreur générale
            throw new Error('Erreur API OpenRouter inattendue: ' + (error.response?.data?.error?.message || error.message));
        }
    }
}

// Exportation de l'instance du service pour une utilisation directe
module.exports = new OcrService();
----------------------------------------------------
server/services/edecGeneratorExport.js

class EdecGeneratorExport {
  generateExport(data) {
    const now = new Date().toISOString();
    const lang = (data.language || 'fr').toLowerCase();

    const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0" createdDate="${now}">
  <goodsDeclarationType>
    <serviceType>2</serviceType>
    <declarationType>2</declarationType>
    <language>${lang}</language>
    <destinationCountry>${data.destination_country}</destinationCountry>

    <transportMeans>
      <transportMode>9</transportMode>
    </transportMeans>

    <exporter>
      <name>${data.user_name}</name>
      ${data.user_address ? `<street>${data.user_address}</street>` : ''}
      <postalCode>${data.user_zip || ''}</postalCode>
      <city>${data.user_city || ''}</city>
      <country>CH</country>
    </exporter>

    <goodsItem>
      <GoodsItemType>
        <description>${data.brand} ${data.model}${data.year ? ' ' + data.year : ''}</description>
        <commodityCode>${data.commodity_code || '8703.9060'}</commodityCode>
        <statisticalCode>${data.statistical_key || '911'}</statisticalCode>
        <additionalUnit>1</additionalUnit>

        <goodsItemDetails>
          <goodsItemDetail>
            <GoodsItemDetail>
              <name>2</name>
              <value>${data.vin}</value>
            </GoodsItemDetail>
          </goodsItemDetail>
        </goodsItemDetails>

        <packaging>
          <PackagingType>
            <packagingType>VN</packagingType>
            <quantity>1</quantity>
          </PackagingType>
        </packaging>
      </GoodsItemType>
    </goodsItem>
  </goodsDeclarationType>
</EdecWeb>`;
    return xml;
  }
}

module.exports = new EdecGeneratorExport();
----------------------------------------------------
server/services/exchangeRateService.js

const mysql = require('mysql2/promise');

class ExchangeRateService {
  constructor() {
    this.dbConfig = {
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME
    };
  }

  async getCurrentRates() {
    const connection = await mysql.createConnection(this.dbConfig);
    try {
      const [rows] = await connection.execute(`
        SELECT currency_code, rate_to_chf, rate_date 
        FROM daily_exchange_rates 
        WHERE rate_date = (
          SELECT MAX(rate_date) FROM daily_exchange_rates
        )
        ORDER BY currency_code
      `);
      const rates = {};
      rows.forEach(r => rates[r.currency_code] = { rate: parseFloat(r.rate_to_chf), date: r.rate_date });
      return rates;
    } finally {
      await connection.end();
    }
  }

  async getRate(currency) {
    if (!currency) return 1.0;
    if (currency === 'CHF') return 1.0;
    const connection = await mysql.createConnection(this.dbConfig);
    try {
      const [rows] = await connection.execute(`
        SELECT rate_to_chf 
        FROM daily_exchange_rates 
        WHERE currency_code = ? 
        ORDER BY rate_date DESC 
        LIMIT 1
      `, [currency]);
      return rows.length > 0 ? parseFloat(rows[0].rate_to_chf) : 1.0;
    } finally {
      await connection.end();
    }
  }

  async convert(amount, fromCurrency, toCurrency = 'CHF') {
    const amt = Number(amount || 0);
    if (!amt) return 0;
    if (fromCurrency === toCurrency) return Math.round(amt * 100) / 100;
    const rateFrom = await this.getRate(fromCurrency);
    const rateTo = await this.getRate(toCurrency);
    const amountInCHF = amt * rateFrom;
    const converted = amountInCHF / rateTo;
    return Math.round(converted * 100) / 100;
  }
}

module.exports = new ExchangeRateService();
----------------------------------------------------
server/services/taresClassifier.js

const fs = require('fs');
const path = require('path');
// Assurez-vous que le chemin d'accès à votre pool de connexion DB est correct
const { pool } = require('../db'); 

/**
 * Gère la logique de classification des véhicules selon les règles TARES
 * et la validation/lookup des VIN.
 * * NOTE: Les règles TARES (taresRules) et la classification s'attendent à être passées
 * par le contrôleur ou lues à l'initialisation du serveur,
 * car la classe n'est plus chargée statiquement par le constructeur.
 */
class TaresClassifier {

    // Suppression du constructeur qui lisait statiquement les fichiers.

    normalizeFuelType(fuelType) {
        if (!fuelType) return 'essence';
        const normalized = fuelType.toLowerCase().trim();
        
        // La nouvelle logique de normalisation est plus simple et utilise des sous-chaînes
        if (normalized.includes('plug')) return 'hybride_plugin';
        if (normalized.includes('hybr')) return 'hybride';
        if (normalized.includes('elec') || normalized.includes('élec')) return 'electrique';
        if (normalized.includes('dies')) return 'diesel';
        if (normalized.includes('ess') || normalized.includes('petrol') || normalized.includes('gasoline')) return 'essence';
        
        return normalized;
    }

    matchesRule(fuel, cylinder, weightEmpty, rule) {
        if (rule.fuel && rule.fuel !== fuel) return false;
        
        if (rule.cylinder_min !== undefined && cylinder < rule.cylinder_min) return false;
        if (rule.cylinder_max !== undefined && cylinder > rule.cylinder_max) return false;
        
        // Utilisation de weightEmpty (poids à vide) pour la classification
        if (rule.weight_min !== undefined && weightEmpty < rule.weight_min) return false;
        if (rule.weight_max !== undefined && weightEmpty > rule.weight_max) return false;
        
        return true;
    }

    /**
     * Tente de classer le véhicule en utilisant les règles de configuration fournies.
     * Si aucune correspondance n'est trouvée, utilise un fallback détaillé.
     * @param {object} vehicleData - Données du véhicule (fuel_type, cc, weight, etc.)
     * @param {object} taresRules - Les règles TARES lues depuis le fichier de configuration (passées par l'appelant)
     */
    async classify(vehicleData, taresRules = {}) {
        const fuelType = this.normalizeFuelType(vehicleData.fuel_type || 'essence');
        const cc = parseInt(vehicleData.cylinder_capacity || 0, 10);
        const wEmpty = parseInt(vehicleData.weight_empty || 0, 10);
        const wTotal = parseInt(vehicleData.weight_total || 0, 10);

        // 1. Tente d'appliquer les règles de configuration d'abord
        for (const [code, rule] of Object.entries(taresRules || {})) {
            if (this.matchesRule(fuelType, cc, wEmpty, rule)) {
                const { statistical_keys = { '911': 'Automobile' }, description = 'Véhicule' } = rule;
                const { key, needs_user_selection, description: skDesc, available_keys } = this.selectStatisticalKey(statistical_keys, wTotal);
                
                return {
                    commodity_code: code,
                    description,
                    statistical_key: needs_user_selection ? null : key,
                    statistical_key_description: needs_user_selection ? null : (skDesc || statistical_keys[key]),
                    needs_user_selection,
                    available_keys: needs_user_selection ? statistical_keys : null
                };
            }
        }

        // 2. Logique de fallback manuelle pour une classification plus précise (comme les règles 8703.xx)
        if (fuelType === 'diesel') {
            if (cc <= 1500) return { commodity_code: '8703.3100', statistical_key: '911', description: 'Véhicule diesel ≤1500 cm3', needs_user_selection: false };
            if (cc > 1500 && cc <= 2500) {
                if (wEmpty <= 1200) return { commodity_code: '8703.3240', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, ≤1200kg', needs_user_selection: false };
                if (wEmpty > 1200 && wEmpty <= 1600) return { commodity_code: '8703.3250', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, >1200kg ≤1600kg', needs_user_selection: false };
                return { commodity_code: '8703.3260', statistical_key: '911', description: 'Diesel >1500 ≤2500 cm3, >1600kg', needs_user_selection: false };
            }
            if (cc > 2500) {
                if (wEmpty <= 1600) return { commodity_code: '8703.3330', statistical_key: '911', description: 'Diesel >2500 cm3, ≤1600kg', needs_user_selection: false };
                return { commodity_code: '8703.3340', statistical_key: '911', description: 'Diesel >2500 cm3, >1600kg', needs_user_selection: false };
            }
        }
        
        // 3. Fallback générique si rien ne correspond
        return {
            commodity_code: '8703.9060',
            statistical_key: '911',
            description: 'Autres véhicules',
            needs_user_selection: false
        };
    }

    selectStatisticalKey(keys, weightTotal) {
        if (!keys || Object.keys(keys).length === 0) return { key: '911', description: 'Automobile', needs_user_selection: false, available_keys: null };
        
        // Cas 1: Une seule clé disponible
        if (Object.keys(keys).length === 1) {
            const k = Object.keys(keys)[0];
            return { key: k, description: keys[k], needs_user_selection: false, available_keys: null };
        }
        
        // Cas 2: Sélection automatique basée sur le Poids Total (ex: 921 > 3500kg, 923 <= 3500kg)
        if (keys['921'] && keys['923']) {
            return weightTotal > 3500
                ? { key: '921', description: keys['921'], needs_user_selection: false, available_keys: null }
                : { key: '923', description: keys['923'], needs_user_selection: false, available_keys: null };
        }
        
        // Cas 3: Sélection manuelle requise
        return { key: null, description: null, needs_user_selection: true, available_keys: keys };
    }

    /**
     * Récupère le pays de fabrication (WMI) à partir du VIN en utilisant la base de données.
     * @param {string} vin - Numéro d'identification du véhicule.
     * @returns {Promise<string|null>} - Code pays (ex: 'FR') ou null.
     */
    async getCountryFromVIN_DB(vin) {
        if (!vin || vin.length < 2) return null;
        const wmi = vin.substring(0, 2).toUpperCase();
        
        // Utilisation du pool pour la connexion à la base de données
        const conn = await pool.getConnection();
        try {
            const [rows] = await conn.execute(`
                SELECT country_code FROM vin_wmi_country_codes
                WHERE ? BETWEEN wmi_start AND wmi_end
                LIMIT 1
            `, [wmi]);
            
            // Assurez-vous d'avoir des colonnes 'wmi_start', 'wmi_end' et 'country_code' dans votre table SQL.
            return rows?.[0]?.country_code || null;
        } catch (error) {
            console.error("Erreur DB WMI:", error.message);
            return null;
        } finally {
            if (conn) conn.release();
        }
    }
}

// Exportation en tant qu'instance unique (Singleton)
module.exports = new TaresClassifier();
----------------------------------------------------
server/db.js

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

module.exports = {
  pool,
  getConnection: () => pool.getConnection()
};
----------------------------------------------------
server/controllers/homeController.js

// Obsolète (SPA), conservé pour compatibilité si utilisé ailleurs
const path = require('path');
const router = require('express').Router();

router.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
});

router.get('/:lang', (req, res) => {
  res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
});

module.exports = router;
----------------------------------------------------
server/controllers/importController.js

// Obsolète, les routes actuelles sont dans /server/routes/api.js
const router = require('express').Router();

router.get('/ping', (req, res) => res.json({ ok: true }));

module.exports = router;
----------------------------------------------------
server/controllers/exportController.js

// Obsolète, les routes actuelles sont dans /server/routes/api.js
module.exports = {
  submit: (req, res) => {
    const xml = `<?xml version="1.0" encoding="UTF-8"?>
<EdecWeb version="4.0">
  <goodsDeclarationType>
    <serviceType>2</serviceType>
  </goodsDeclarationType>
</EdecWeb>`;
    res.set('Content-Type', 'application/xml');
    res.set('Content-Disposition', `attachment; filename="edec-export-${Date.now()}.xml"`);
    res.send(xml);
  }
};
----------------------------------------------------
public/js/controllers/homeController.js

angular.module('edecApp')
  .controller('HomeController', function($scope) {
    $scope.features = [
      { icon: '⚡', title: 'Rapide', description: 'Générez votre déclaration en moins de 5 minutes' },
      { icon: '💰', title: 'Économique', description: 'Tarifs transparents, outil personnel' },
      { icon: '✅', title: 'Simple', description: 'Interface intuitive, étapes guidées' },
      { icon: '🤖', title: 'IA intégrée', description: 'Extraction automatique depuis votre carte grise' }
    ];
  });
----------------------------------------------------
public/js/controllers/importController.js

angular.module('edecApp')
  .controller('ImportController', function($scope, $http, $window, $rootScope) {
    // Initialisation des variables
    $scope.step = 1;
    $scope.method = null;
    $scope.vehicle = {};
    $scope.parties = {
      importer: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' },
      consignee: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' }, // Ajout de firstname pour les parties
      declarant: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' } // Ajout de firstname pour les parties
    };
    $scope.business = { cash_payment: false };
    $scope.declaration = {
      transport_mode: '9',
      purchase_currency: 'EUR',
      purchase_price: 0,
      transport_cost: 0,
      other_costs: 0,
      language: $rootScope.lang || 'fr',
      dispatch_country: ''
    };
    $scope.countries = {};
    $scope.currencies = {};
    $scope.classification = null;
    $scope.ocrLoading = false;
    $scope.generating = false;
    // [NOUVEAU] Drapeau pour indiquer si une tentative d'OCR a eu lieu
    $scope.ocrAttempted = false;
    $scope.statisticalValueCHF = 0;
    $scope.vatValueCHF = 0;
    $scope.vinValid = null;

    // Nouvelle variable pour gérer l'état du champ prénom dans la vue (ng-disabled)
    $scope.isCompany = false;

    // NOUVELLES VARIABLES DE STATUT DE SOUMISSION
    $scope.sessionToken = null;
    $scope.submissionProgress = 0;
    $scope.submissionStep = '';
    $scope.showProgress = false;
    // FIN NOUVELLES VARIABLES
    
    // Load configs
    $http.get('/api/countries').then(function(res) {
      $scope.countries = res.data;
    });
    $http.get('/api/currencies').then(function(res) {
      $scope.currencies = res.data;
    });

    // NOUVELLE LOGIQUE: Surveiller l'IDE pour déterminer si c'est une société et griser le prénom
    $scope.$watch('parties.importer.ide', function(newVal) {
      // Si l'IDE est rempli (et différent de la valeur par défaut pour particulier, si applicable), on suppose que c'est une société.
      $scope.isCompany = !!newVal && newVal.trim() !== 'CHE222251936';
      if ($scope.isCompany) {
        // Optionnel : vider le prénom si on passe en mode société, mais l'utilisateur a demandé de ne pas vider, donc on laisse le contenu.
      }
    });

    $scope.selectMethod = function(method) {
      $scope.method = method;
      $scope.step = 2;
    };

    $scope.triggerFileInput = function() {
      document.getElementById('file-upload').click();
    };

    // OCR upload
    $scope.handleFileSelect = function(event) {
      const files = event.target.files;
      if (!files || files.length === 0) {
        return;
      }

      $scope.$apply(function() {
        $scope.ocrLoading = true;
        // [MODIFICATION] On marque la tentative au début de la fonction
        $scope.ocrAttempted = true;
      });

      const formData = new FormData();
      formData.append('registration_card', files[0]);

      $http.post('/api/ocr', formData, {
        transformRequest: angular.identity,
        headers: { 'Content-Type': undefined }
      })
      .then(function(response) {
        Object.assign($scope.vehicle, response.data);
        $scope.ocrLoading = false;

        // [MODIFICATION] Si l'OCR réussit, on peut considérer qu'il n'y a pas d'échec d'affichage du message d'erreur
        // Bien que 'ocrAttempted' soit déjà true, on ne le remet pas à false pour garder l'info de la tentative.

        if ($scope.vehicle.vin) {
          $scope.validateVIN();
        }
        $scope.updateClassification();
      })
      .catch(function(error) {
        const details = error.data?.details || error.data?.error || 'Erreur inconnue';
        const hint = error.data?.hint || '';
        alert('Erreur OCR: ' + details + (hint ? '\n\n' + hint : ''));
        console.error('Détails complets:', error.data);
        $scope.ocrLoading = false;

        // [NOTE]: L'erreur sera affichée dans la vue car ocrAttempted est true 
        // et $scope.vehicle est resté vide ou a été vidé lors de l'échec.
      });
    };

    $scope.validateVIN = function() {
      if (!$scope.vehicle.vin || $scope.vehicle.vin.length !== 17) {
        $scope.vinValid = false;
        return;
      }
      $http.post('/api/validate-vin', { vin: $scope.vehicle.vin })
        .then(function(response) {
          $scope.vinValid = response.data.valid;
          if (response.data.valid && response.data.country) {
            $scope.declaration.dispatch_country = response.data.country;
          }
        });
    };

    $scope.updateClassification = function() {
      if (!$scope.vehicle.fuel_type || !$scope.vehicle.weight_empty) {
        return;
      }
      $http.post('/api/classify-vehicle', $scope.vehicle)
        .then(function(response) {
          $scope.classification = response.data;
          $scope.vehicle.commodity_code = response.data.commodity_code;
          if (!response.data.needs_user_selection) {
            $scope.vehicle.statistical_key = response.data.statistical_key;
          }
        });
    };

    // CORRECTION: Assure que les valeurs CHF sont calculées et arrondies à l'entier
    $scope.calculateTotal = function() {
      const price = parseFloat($scope.declaration.purchase_price) || 0;
      const transport = parseFloat($scope.declaration.transport_cost) || 0;
      const other = parseFloat($scope.declaration.other_costs) || 0;
      const currency = $scope.declaration.purchase_currency;

      // 1. Valeur Statistique (Statistical Value) = Prix d'achat seul
      $http.post('/api/convert-currency', {
        amount: price,
        from: currency,
        to: 'CHF'
      })
      .then(function(response) {
        // Conversion en entier obligatoire
        $scope.statisticalValueCHF = Math.round(response.data.converted);

        // 2. Base TVA (VAT Value) = Prix d'achat + Transport + Autres frais
        return $http.post('/api/convert-currency', {
          amount: price + transport + other,
          from: currency,
          to: 'CHF'
        });
      })
      .then(function(responseVat) {
        // Conversion en entier obligatoire
        $scope.vatValueCHF = Math.round(responseVat.data.converted);
      });
    };

    $scope.nextStep = function() {
      if ($scope.step === 2) {
        if (!$scope.vehicle.vin || !$scope.vehicle.brand || !$scope.vehicle.model || !$scope.vehicle.weight_empty || !$scope.vehicle.weight_total) {
          alert('Veuillez remplir les informations du véhicule');
          return;
        }
      }
      $scope.step++;
    };

    $scope.prevStep = function() {
      $scope.step--;
    };

    $scope.transportModes = [
      { value: '9', label: 'Autopropulsion' },
      { value: '7', label: 'Pipeline, etc.' },
      { value: '4', label: 'Trafic aérien' },
      { value: '2', label: 'Trafic ferroviaire' },
      { value: '8', label: 'Trafic par eau' },
      { value: '3', label: 'Trafic routier' }
    ];

    // FONCTION UTILITAIRE: Décide si on concatène Nom et Prénom ou si on prend juste le Nom de la société.
    const getFullName = function(name, firstname, ide) {
      // Vérifie si l'IDE est rempli et n'est pas l'IDE par défaut pour particulier
      const isCompany = !!ide && ide.trim() !== 'CHE222251936';

      if (isCompany) {
        return name || ''; // Retourne la raison sociale
      }
      // Pour les particuliers, on concatène (Prénom Nom)
      const parts = [firstname, name].filter(function(p) { return p && p.trim(); });
      return parts.join(' ');
    };

    // =========================================================================
    // ROUTE MODIFIÉE : Lancement de la génération et du polling
    // =========================================================================
    $scope.generateDeclaration = function() {
      if ($scope.generating) return;
      
      const imp = $scope.parties.importer;
      if (!imp.name || !imp.zip || !imp.city) {
        alert('Veuillez renseigner les infos Importateur');
        return;
      }
      if (!$scope.declaration.dispatch_country) {
        alert('Veuillez sélectionner le pays d\'expédition');
        return;
      }

      $scope.generating = true;
      $scope.submissionProgress = 0;
      $scope.submissionStep = 'Initialisation';
      $scope.showProgress = true;

      // Détermination du nom formaté pour l'Importateur/User
      const importerFullName = getFullName(imp.name, imp.firstname, imp.ide);

      // Détermination du nom pour le Consignataire (prend ceux de l'Importateur si vides)
      const cons = $scope.parties.consignee;
      const consigneeIsImporter = !cons.name && !cons.ide;
      const consigneeName = consigneeIsImporter
        ? importerFullName
        : getFullName(cons.name, cons.firstname, cons.ide);
      const consigneeIde = cons.ide || imp.ide;

      // Détermination du nom pour le Déclarant (prend ceux de l'Importateur si vides)
      const decl = $scope.parties.declarant;
      const declarantName = getFullName(
        decl.name || imp.name,
        decl.firstname || imp.firstname,
        decl.ide || imp.ide
      );

      const payload = Object.assign({}, $scope.vehicle, $scope.declaration, {
        user_name: importerFullName,
        user_address: imp.address,
        user_zip: imp.zip,
        user_city: imp.city,
        user_ide: imp.ide,

        consignee_name: consigneeName,
        consignee_address: cons.address || imp.address,
        consignee_zip: cons.zip || imp.zip,
        consignee_city: cons.city || imp.city,
        consignee_ide: consigneeIde,

        declarant_name: declarantName,
        declarant_address: decl.address || imp.address,
        declarant_zip: decl.zip || imp.zip,
        declarant_city: decl.city || imp.city,
        declarant_ide: decl.ide || '',

        cash_payment: $scope.business.cash_payment ? 1 : 0,

        statistical_value_chf: $scope.statisticalValueCHF,
        vat_value_chf: $scope.vatValueCHF,

        language: $rootScope.lang
      });
        
      console.log('[CLIENT] Envoi demande génération...');
      
      $http.post('/api/generate-import', payload)
        .then(function(response) {
          console.log('[CLIENT] Génération acceptée, session:', response.data.sessionToken);
          $scope.sessionToken = response.data.sessionToken;
          
          // Lancer le polling du statut
          $scope.pollSubmissionStatus();
        })
        .catch(function(error) {
          alert('Erreur: ' + (error.data?.error || 'Erreur de génération'));
          $scope.generating = false;
          $scope.showProgress = false;
        });
    };
    // =========================================================================
    
    // =========================================================================
    // NOUVELLE MÉTHODE : Polling du statut de soumission
    // =========================================================================
    $scope.pollSubmissionStatus = function() {
        const poll = function() {
            if (!$scope.sessionToken) return;
            
            $http.get('/api/submission-status/' + $scope.sessionToken)
                .then(function(response) {
                    const status = response.data;
                    
                    $scope.submissionProgress = status.progress;
                    $scope.submissionStep = status.step;
                    
                    console.log(`[CLIENT] Statut: ${status.status}, Progression: ${status.progress}%`);
                    
                    if (status.status === 'submitted') {
                        // Soumission terminée avec succès
                        alert('✅ Déclaration soumise avec succès!\nNuméro: ' + status.declarationNumber);
                        $scope.downloadSubmissionFiles(status);
                        $scope.step = 4;
                        $scope.generating = false;
                        $scope.showProgress = false;
                    } else if (status.status === 'submission_error') {
                        // Erreur
                        alert('⚠️ Erreur lors de la soumission: ' + status.error);
                        $scope.generating = false;
                        $scope.showProgress = false;
                    } else {
                        // Continuer le polling
                        setTimeout(poll, 2000);
                    }
                })
                .catch(function(error) {
                    console.error('[CLIENT] Erreur polling:', error);
                    $scope.submissionStep = 'Erreur réseau/serveur. Tentative de reconnexion...';
                    setTimeout(poll, 5000); // Réessayer après 5s en cas d'erreur
                });
        };
        
        poll(); // Premier appel
    };
    // =========================================================================

    // =========================================================================
    // NOUVELLE MÉTHODE : Téléchargement des fichiers PDF
    // =========================================================================
    $scope.downloadSubmissionFiles = function(status) {
        console.log('[CLIENT] Téléchargement des fichiers PDF...');
        
        // Télécharger la liste d'importation
        if (status.listePath) {
            const a1 = document.createElement('a');
            a1.href = '/api/download-pdf?path=' + encodeURIComponent(status.listePath);
            a1.download = 'liste_importation_' + $scope.sessionToken + '.pdf';
            document.body.appendChild(a1);
            a1.click();
            document.body.removeChild(a1);
        }
        
        // Télécharger le bulletin de délivrance
        if (status.bulletinPath) {
            setTimeout(() => {
                const a2 = document.createElement('a');
                a2.href = '/api/download-pdf?path=' + encodeURIComponent(status.bulletinPath);
                a2.download = 'bulletin_delivrance_' + $scope.sessionToken + '.pdf';
                document.body.appendChild(a2);
                a2.click();
                document.body.removeChild(a2);
            }, 1000); // Délai pour ne pas bloquer le premier téléchargement
        }
    };
    // =========================================================================
    
    // REMPLACEMENT : Suppression de la fonction submitToEdec, car elle n'est plus utilisée.
    /*
    $scope.submitToEdec = function(sessionToken) {
    // ... code de soumission obsolète ...
    };
    */

    $scope.resetForm = function() {
      $scope.step = 1;
      $scope.method = null;
      $scope.vehicle = {};
      $scope.parties = {
        importer: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' },
        consignee: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' },
        declarant: { name: '', firstname: '', address: '', zip: '', city: '', ide: '' }
      };
      $scope.business = { cash_payment: false };
      $scope.declaration = {
        transport_mode: '9',
        purchase_currency: 'EUR',
        purchase_price: 0,
        transport_cost: 0,
        other_costs: 0,
        language: $rootScope.lang || 'fr',
        dispatch_country: ''
      };
      $scope.classification = null;
      $scope.statisticalValueCHF = 0;
      $scope.vatValueCHF = 0;
      $scope.vinValid = null;
      $scope.isCompany = false;
      // [MODIFICATION] Réinitialisation de l'état
      $scope.ocrAttempted = false;
        
      // RÉINITIALISATION DES VARIABLES DE SOUMISSION
      $scope.sessionToken = null;
      $scope.submissionProgress = 0;
      $scope.submissionStep = '';
      $scope.showProgress = false;
      // FIN RÉINITIALISATION
    };
  });
----------------------------------------------------
public/js/controllers/exportController.js

angular.module('edecApp')
  .controller('ExportController', function($scope, $http, $window, $rootScope) {
    $scope.vehicle = {};
    $scope.export = { export_type: 'temporary', language: $rootScope.lang || 'fr' };
    $scope.parties = { exporter: { name: '', address: '', zip: '', city: '', ide: '' } };
    $scope.countries = {};
    $scope.generating = false;

    $http.get('/api/countries').then(res => $scope.countries = res.data);

    $scope.generateExport = function() {
      if ($scope.generating) return;
      const ex = $scope.parties.exporter;
      if (!ex.name || !ex.zip || !ex.city) { alert('Veuillez renseigner les infos Exportateur'); return; }
      if (!$scope.export.destination_country) { alert('Veuillez sélectionner le pays de destination'); return; }

      $scope.generating = true;
      const data = Object.assign({}, $scope.vehicle, $scope.export, {
        user_name: ex.name,
        user_address: ex.address,
        user_zip: ex.zip,
        user_city: ex.city,
        user_ide: ex.ide
      });

      $http.post('/api/generate-export', data, { responseType: 'blob' })
        .then(function(response) {
          const blob = new Blob([response.data], { type: 'application/xml' });
          const url = $window.URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = 'edec-export-' + Date.now() + '.xml';
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          $window.URL.revokeObjectURL(url);
          alert('✅ Déclaration d\'export générée avec succès !');
          $scope.generating = false;
        }).catch(function(error) {
          alert('Erreur: ' + (error.data?.error || 'Erreur de génération'));
          $scope.generating = false;
        });
    };
  });
----------------------------------------------------
public/js/app.js

angular.module('edecApp', ['ngRoute'])
  .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
    $locationProvider.html5Mode({ enabled: true, requireBase: true });

    $routeProvider
      .when('/', { templateUrl: 'views/home.html', controller: 'HomeController' })
      .when('/import', { templateUrl: 'views/import.html', controller: 'ImportController' })
      .when('/export', { templateUrl: 'views/export.html', controller: 'ExportController' })
      .otherwise({ redirectTo: '/' });
  }])
  .controller('MainController', ['$scope', '$location', function($scope, $location) {
    $scope.currentPath = function() { return $location.path(); };
  }])
  .run(['$rootScope', function($rootScope) {
    // Language handling
    const SUPPORTED = ['fr', 'de', 'it', 'en'];
    const stored = localStorage.getItem('edec_lang');
    const defaultLang = 'fr';
    $rootScope.lang = SUPPORTED.includes(stored) ? stored : defaultLang;
    $rootScope.setLang = function(l) {
      if (SUPPORTED.includes(l)) {
        $rootScope.lang = l;
        localStorage.setItem('edec_lang', l);
      }
    };
    $rootScope.isLang = function(l) { return $rootScope.lang === l; };

    // Basic dictionary (minimal; page reste FR par défaut mais on met à jour XML/langue)
    $rootScope.t = function(s) { return s; };

    $rootScope.apiUrl = '/api';
  }]);
----------------------------------------------------
public/index.html

<!DOCTYPE html>
<html lang="fr" ng-app="edecApp">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>e-dec Véhicules - Déclaration Simplifiée</title>
  <base href="/">
  <link rel="stylesheet" href="/css/style.css">
  
  <!-- AngularJS CDN -->
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular-route.min.js"></script>
</head>
<body ng-controller="MainController">
  <!-- Navigation -->
  <nav class="navbar">
    <div class="container">
      <a href="/" class="logo">🚗 e-dec Véhicules</a>
      <ul class="nav-menu">
        <a href="/import" ng-class="{active: currentPath() == '/import'}">Importer</a>
        <a href="/export" ng-class="{active: currentPath() == '/export'}">Exporter</a>
        <a href="/" ng-class="{active: currentPath() == '/'}">Accueil</a>
      </ul>
      <div class="lang-switcher">
        <button class="btn-lang" ng-class="{active: isLang('fr')}" ng-click="setLang('fr')">FR</button>
        <button class="btn-lang" ng-class="{active: isLang('de')}" ng-click="setLang('de')">DE</button>
        <button class="btn-lang" ng-class="{active: isLang('it')}" ng-click="setLang('it')">IT</button>
        <button class="btn-lang" ng-class="{active: isLang('en')}" ng-click="setLang('en')">EN</button>
      </div>
    </div>
  </nav>
  
  <!-- Contenu principal -->
  <main class="main-content" ng-view></main>
  
  <!-- Footer -->
  <footer class="footer">
    <div class="container">
      <p>&copy; 2025 e-dec Véhicules. Simplifiez vos déclarations douanières.</p>
      <p class="disclaimer">⚠️ Cet outil aide à créer votre déclaration. La responsabilité des informations incombe au déclarant.</p>
    </div>
  </footer>
  
  <!-- Scripts AngularJS -->
    <script src="/js/app.js"></script>
    <script src="/js/controllers/homeController.js"></script>
    <script src="/js/controllers/importController.js"></script>
    <script src="/js/controllers/exportController.js"></script>

</body>
</html>
----------------------------------------------------
public/css/style.css

/* Reset & Base */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f5f7fa;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

/* Navigation */
.navbar {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 1rem 0;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.navbar .container {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: white;
    text-decoration: none;
}

.nav-menu {
    display: flex;
    list-style: none;
    gap: 2rem;
}

.nav-menu a {
    color: rgba(255,255,255,0.9);
    text-decoration: none;
    font-weight: 500;
    transition: color 0.3s;
}

.nav-menu a:hover,
.nav-menu a.active {
    color: white;
}

.lang-switcher {
    display: flex;
    gap: 0.5rem;
}

.btn-lang {
    padding: 0.5rem 1rem;
    border: 1px solid rgba(255,255,255,0.3);
    background: rgba(255,255,255,0.1);
    color: white;
    cursor: pointer;
    border-radius: 5px;
    transition: all 0.3s;
}

.btn-lang:hover,
.btn-lang.active {
    background: rgba(255,255,255,0.2);
    border-color: white;
}

/* Hero */
.hero {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 5rem 0;
    text-align: center;
}

.hero h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.subtitle {
    font-size: 1.25rem;
    margin-bottom: 2rem;
    opacity: 0.9;
}

.cta-buttons {
    display: flex;
    gap: 1rem;
    justify-content: center;
    flex-wrap: wrap;
}

/* Buttons */
.btn {
    padding: 0.875rem 2rem;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: all 0.3s;
    display: inline-block;
}

.btn-primary {
    background: #667eea;
    color: white;
}

.btn-primary:hover {
    background: #5568d3;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(102,126,234,0.4);
}

.btn-secondary {
    background: #6c757d;
    color: white;
}

.btn-secondary:hover {
    background: #5a6268;
}

/* Features */
.features {
    padding: 5rem 0;
    background: white;
}

.features h2 {
    text-align: center;
    margin-bottom: 3rem;
    font-size: 2.5rem;
    color: #333;
}

.features-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 2rem;
}

.feature-card {
    text-align: center;
    padding: 2rem;
    border-radius: 12px;
    background: #f8f9fa;
    transition: transform 0.3s;
}

.feature-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}

.feature-icon {
    font-size: 3rem;
    margin-bottom: 1rem;
}

/* How it works */
.how-it-works {
    padding: 5rem 0;
}

.how-it-works h2 {
    text-align: center;
    margin-bottom: 3rem;
    font-size: 2.5rem;
}

.steps {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 2rem;
}

.step-item {
    text-align: center;
}

.step-number {
    width: 60px;
    height: 60px;
    background: #667eea;
    color: white;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    font-weight: bold;
    margin: 0 auto 1rem;
}

/* Progress Bar */
.progress-bar {
    display: flex;
    justify-content: space-between;
    margin: 2rem 0 3rem;
    position: relative;
}

.progress-bar::before {
    content: '';
    position: absolute;
    top: 20px;
    left: 0;
    right: 0;
    height: 2px;
    background: #ddd;
    z-index: 0;
}

.progress-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    z-index: 1;
}

.step-num {
    width: 40px;
    height: 40px;
    background: #ddd;
    color: #666;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

.progress-step.active .step-num {
    background: #667eea;
    color: white;
}

.progress-step.completed .step-num {
    background: #28a745;
    color: white;
}

/* Method Cards */
.method-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
}

.method-card {
    text-align: center;
    padding: 3rem 2rem;
    border: 2px solid #ddd;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.3s;
}

.method-card:hover {
    border-color: #667eea;
    transform: translateY(-5px);
    box-shadow: 0 8px 20px rgba(102,126,234,0.2);
}

.method-icon {
    font-size: 4rem;
    margin-bottom: 1rem;
}

/* Form */
.form {
    max-width: 800px;
    margin: 2rem auto;
    background: white;
    padding: 2rem;
    border-radius: 12px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

fieldset {
    border: 1px solid #ddd;
    padding: 1.5rem;
    margin-bottom: 2rem;
    border-radius: 8px;
}

legend {
    font-weight: bold;
    padding: 0 0.5rem;
    color: #667eea;
}

.form-group {
    margin-bottom: 1.5rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
    color: #333;
}

.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
}

/* Style des inputs: inclut text, email, number et select */
input[type="text"],
input[type="email"],
input[type="number"],
select {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 1rem;
    transition: border-color 0.3s;
}

input:focus,
select:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
}

.hint {
    display: block;
    font-size: 0.875rem;
    color: #28a745;
    margin-top: 0.25rem;
}

.error {
    display: block;
    font-size: 0.875rem;
    color: #dc3545;
    margin-top: 0.25rem;
}

/* Upload Box */
.upload-box {
    border: 3px dashed #ddd;
    border-radius: 12px;
    padding: 3rem;
    text-align: center;
    margin: 2rem 0;
    cursor: pointer;
    transition: all 0.3s;
}

.upload-box:hover {
    border-color: #667eea;
    background: #f8f9fa;
}

.upload-label {
    font-size: 1.25rem;
    cursor: pointer;
}

/* Classification Info */
.classification-info {
    background: #e7f3ff;
    border-left: 4px solid #667eea;
    padding: 1.5rem;
    margin: 1.5rem 0;
    border-radius: 6px;
}

/* Total Box */
.total-box {
    background: #d4edda;
    border: 2px solid #28a745;
    padding: 1.5rem;
    border-radius: 8px;
    margin: 1.5rem 0;
    text-align: center;
}

.total-box h3 {
    color: #155724;
    margin: 0;
}

/* Checkbox */
.checkbox-label {
    display: flex;
    align-items: center;
    cursor: pointer;
    margin: 1rem 0;
}

.checkbox-label input[type="checkbox"] {
    width: auto;
    margin-right: 0.75rem; /* Mieux */
    height: 1.1em;         /* Mieux */
    width: 1.1em;          /* Mieux */
}

/* Form Actions */
.form-actions {
    display: flex;
    gap: 1rem;
    justify-content: space-between; /* Mieux pour la navigation Précédent/Suivant */
    margin-top: 2rem;
}

/* Success */
.success-content {
    text-align: center;
    padding: 3rem 0;
}

.success-icon {
    font-size: 5rem;
    margin-bottom: 1rem;
    color: #28a745; /* Mieux: couleur de succès */
}

.next-steps-box {
    background: white;
    border: 1px solid #ddd;
    border-radius: 12px;
    padding: 2rem;
    margin: 2rem auto;
    max-width: 600px;
    text-align: left;
}

.next-steps-box ol {
    padding-left: 1.5rem; /* Mieux que margin-left */
}

.next-steps-box li {
    margin-bottom: 0.5rem;
}

/* Barre de progression */
.progress-bar-container {
  width: 100%;
  height: 20px;
  background: #ddd;
  border-radius: 10px;
  margin: 1rem 0;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  transition: width 0.5s ease;
}

.progress-section {
  text-align: center;
  padding: 2rem 0;
}

/* Footer */
.footer {
    background: #2c3e50;
    color: white;
    padding: 2rem 0;
    text-align: center;
    margin-top: 3rem;
}

.disclaimer {
    font-size: 0.875rem;
    opacity: 0.8;
    margin-top: 0.5rem;
}

/* Main Content */
.main-content {
    min-height: calc(100vh - 300px);
    padding: 2rem 0;
}

/* Responsive */
@media (max-width: 768px) {
    .hero h1 {
        font-size: 2rem;
    }
    
    .navbar .container {
        flex-direction: column;
        gap: 1rem;
    }
    
    .nav-menu {
        flex-direction: column;
        gap: 0.5rem;
        text-align: center;
        width: 100%;
    }
    
    .form-row {
        grid-template-columns: 1fr;
    }
    
    .form-actions {
        flex-direction: column-reverse; /* Mieux: met le bouton principal en bas */
    }
    
    .btn {
        width: 100%;
    }
}
----------------------------------------------------
public/views/home.html

<div class="hero">
    <div class="container">
        <h1>🚗 Déclaration e-dec Simplifiée</h1>
        <p class="subtitle">Importez ou exportez votre véhicule en Suisse en quelques clics</p>
        <div class="cta-buttons">
            <a href="/import" class="btn btn-primary">Importer un véhicule</a>
            <a href="/export" class="btn btn-secondary">Exporter un véhicule</a>
        </div>
    </div>
</div>

<section class="features">
    <div class="container">
        <h2>Pourquoi choisir notre service ?</h2>
        <div class="features-grid">
            <div class="feature-card" ng-repeat="feature in features">
                <div class="feature-icon">{{ feature.icon }}</div>
                <h3>{{ feature.title }}</h3>
                <p>{{ feature.description }}</p>
            </div>
        </div>
    </div>
</section>

<section class="how-it-works">
    <div class="container">
        <h2>Comment ça marche ?</h2>
        <div class="steps">
            <div class="step-item">
                <div class="step-number">1</div>
                <h3>Choisissez votre méthode</h3>
                <p>Scanner la carte grise ou saisie manuelle</p>
            </div>
            <div class="step-item">
                <div class="step-number">2</div>
                <h3>Remplissez les informations</h3>
                <p>Véhicule, finances et coordonnées</p>
            </div>
            <div class="step-item">
                <div class="step-number">3</div>
                <h3>Générez le fichier XML</h3>
                <p>Téléchargez votre déclaration e-dec prête à l'emploi</p>
            </div>
            <div class="step-item">
                <div class="step-number">4</div>
                <h3>Soumettez à la douane</h3>
                <p>Importez le fichier sur le portail officiel e-dec</p>
            </div>
        </div>
    </div>
</section>
----------------------------------------------------
public/views/export.html

<div class="container">
    <h1>Exporter un Véhicule depuis la Suisse</h1>
    <p>Cette page vous permet de générer une déclaration d'exportation pour un véhicule sortant du territoire suisse.</p>
    
    <form name="exportForm" class="form" ng-submit="generateExport()" novalidate>
        <fieldset>
            <legend>Informations Exportateur</legend>
            <div class="form-row">
                <div class="form-group">
                    <label>Nom ou raison sociale *</label>
                    <input type="text" ng-model="parties.exporter.name" required>
                </div>
                 <div class="form-group">
                    <label>Numéro IDE</label>
                    <input type="text" ng-model="parties.exporter.ide" placeholder="Optionnel">
                </div>
            </div>
            <div class="form-group">
                <label>Adresse</label>
                <input type="text" ng-model="parties.exporter.address">
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label>NPA *</label>
                    <input type="text" ng-model="parties.exporter.zip" required>
                </div>
                <div class="form-group">
                    <label>Ville *</label>
                    <input type="text" ng-model="parties.exporter.city" required>
                </div>
            </div>
        </fieldset>

        <fieldset>
            <legend>Informations Véhicule</legend>
            <div class="form-group">
                <label>VIN (Numéro de châssis) *</label>
                <input type="text" ng-model="vehicle.vin" maxlength="17" minlength="17" required style="text-transform:uppercase">
            </div>
            
            <div class="form-row">
                <div class="form-group">
                    <label>Marque *</label>
                    <input type="text" ng-model="vehicle.brand" required>
                </div>
                <div class="form-group">
                    <label>Modèle *</label>
                    <input type="text" ng-model="vehicle.model" required>
                </div>
                 <div class="form-group">
                    <label>Année (optionnel)</label>
                    <input type="number" ng-model="vehicle.year">
                </div>
            </div>
        </fieldset>
        
        <fieldset>
            <legend>Informations d'Exportation</legend>
            <div class="form-group">
                <label>Pays de destination *</label>
                <select ng-model="export.destination_country" required 
                        ng-options="code as name for (code, name) in countries">
                    <option value="">Sélectionner un pays...</option>
                </select>
            </div>
            
            <div class="form-group">
                <label>Type d'exportation (informatif)</label>
                <select ng-model="export.export_type">
                    <option value="temporary">Temporaire (retour prévu)</option>
                    <option value="definitive">Définitive</option>
                </select>
            </div>
        </fieldset>
        
        <div class="form-actions" style="justify-content: flex-end;">
            <button type="submit" class="btn btn-primary" ng-disabled="exportForm.$invalid || generating">
                <span ng-if="!generating">Générer la déclaration ✨</span>
                <span ng-if="generating">Génération en cours...</span>
            </button>
        </div>
    </form>
</div>
----------------------------------------------------
public/views/import.html

<div class="container">
  <h1>Importer un Véhicule en Suisse</h1>
  <div class="progress-bar" ng-show="step > 1">
    <div class="progress-step" ng-class="{active: step >= 1, completed: step > 1}">
      <span class="step-num">1</span>
      <span class="step-label">Méthode</span>
    </div>
    <div class="progress-step" ng-class="{active: step >= 2, completed: step > 2}">
      <span class="step-num">2</span>
      <span class="step-label">Véhicule</span>
    </div>
    <div class="progress-step" ng-class="{active: step >= 3, completed: step > 3}">
      <span class="step-num">3</span>
      <span class="step-label">Déclaration</span>
    </div>
    <div class="progress-step" ng-class="{active: step >= 4}">
      <span class="step-num">4</span>
      <span class="step-label">Résultat</span>
    </div>
  </div>
  <div ng-show="step === 1" class="step-content">
    <h2 style="text-align: center; margin-bottom: 2rem;">Choisissez votre méthode de saisie</h2>
    <div class="method-cards">
      <div class="method-card" ng-click="selectMethod('ocr')">
        <div class="method-icon">📸</div>
        <h3>Scanner la carte grise</h3>
        <p>Extraction automatique des données par IA (JPG, PNG, PDF)</p>
      </div>
      <div class="method-card" ng-click="selectMethod('manual')">
        <div class="method-icon">⌨️</div>
        <h3>Saisie manuelle</h3>
        <p>Remplir le formulaire pas à pas</p>
      </div>
    </div>
  </div>
  <div ng-show="step === 2" class="step-content">
    <h2 style="text-align: center;">Informations du véhicule</h2>
    <div ng-if="method === 'ocr'" class="ocr-section">
      <div class="upload-box" ng-click="triggerFileInput()">
        <label class="upload-label">
          <span ng-if="!ocrLoading">📷 Photographier ou choisir l'image/PDF</span>
          <span ng-if="ocrLoading">⏳ Extraction en cours...</span>
        </label>
        <input id="file-upload" type="file" accept="image/*,application/pdf" style="display:none" onchange="angular.element(this).scope().handleFileSelect(event)">
      </div>
    </div>
    <form name="vehicleForm" class="form" ng-show="method === 'manual' || vehicle.vin || vehicle.brand" novalidate>
      <div class="form-group">
        <label>VIN (Numéro de châssis / 17 caractères) *</label>
        <input type="text" ng-model="vehicle.vin" maxlength="17" minlength="17" ng-change="validateVIN()" required style="text-transform:uppercase">
        <span class="hint" ng-if="vinValid">✅ VIN valide. Pays d'origine détecté: {{declaration.dispatch_country || '...'}}</span>
        <span class="error" ng-if="vinValid === false">❌ Le format du VIN est invalide (17 caractères alphanumériques, sans I, O, Q).</span>
      </div>
      <div class="form-row">
        <div class="form-group">
          <label>Marque *</label>
          <input type="text" ng-model="vehicle.brand" required>
        </div>
        <div class="form-group">
          <label>Modèle *</label>
          <input type="text" ng-model="vehicle.model" required>
        </div>
      </div>
      <div class="form-row">
        <div class="form-group">
          <label>Année de 1ère mise en circulation</label>
          <input type="number" ng-model="vehicle.year" min="1900" max="2026">
        </div>
        <div class="form-group">
          <label>Type de carburant *</label>
          <select ng-model="vehicle.fuel_type" ng-change="updateClassification()" required>
            <option value="">Sélectionner...</option>
            <option value="essence">Essence</option>
            <option value="diesel">Diesel</option>
            <option value="electrique">Électrique</option>
            <option value="hybride">Hybride</option>
            <option value="hybride_plugin">Hybride rechargeable</option>
          </select>
        </div>
      </div>
      <div class="form-row">
        <div class="form-group">
          <label>Cylindrée (cm³)</label>
          <input type="number" ng-model="vehicle.cylinder_capacity" ng-change="updateClassification()">
        </div>
        <div class="form-group">
          <label>Poids à vide (kg) *</label>
          <input type="number" ng-model="vehicle.weight_empty" ng-change="updateClassification()" required>
        </div>
        <div class="form-group">
          <label>Poids total autorisé (kg) *</label>
          <input type="number" ng-model="vehicle.weight_total" ng-change="updateClassification()" required>
        </div>
      </div>
      <div class="classification-info" ng-if="classification">
        <h4>📋 Classification douanière estimée</h4>
        <p>
          <strong>Code TARES:</strong> {{ classification.commodity_code }} - {{ classification.description }}
        </p>
        <div ng-if="classification.needs_user_selection">
          <p>
            <strong>Sélectionnez la clé statistique: *</strong>
          </p>
          <label ng-repeat="(key, desc) in classification.available_keys" class="radio-label">
            <input type="radio" ng-model="vehicle.statistical_key" ng-value="key" required>
            {{ key }} - {{ desc }}
          </label>
        </div>
        <p ng-if="!classification.needs_user_selection">
          <strong>Clé statistique:</strong> {{ vehicle.statistical_key }} - {{ classification.statistical_key_description }}
        </p>
      </div>
      <div class="form-actions">
        <button type="button" class="btn btn-secondary" ng-click="step = 1">← Retour</button>
        <button type="button" class="btn btn-primary" ng-click="nextStep()" ng-disabled="vehicleForm.$invalid || (classification.needs_user_selection && !vehicle.statistical_key)">Continuer →</button>
      </div>
    </form>
    <div class="alert alert-danger" ng-if="ocrAttempted && !ocrLoading && !vehicle.vin && !vehicle.brand && !vehicle.model"> Analyse de votre carte grise impossible. Veuillez vérifier le fichier et réessayer. </div>
  </div>
  <div ng-show="step === 3" class="step-content">
    <h2 style="text-align: center;">Informations pour la déclaration</h2>
    <form name="valuationForm" class="form" novalidate>
      <fieldset>
        <legend>Parties Impliquées</legend>
        <h4>Importateur / Destinataire</h4>
        <p class="disclaimer" style="margin-bottom: 1rem;">La personne ou l'entreprise qui importe le véhicule en Suisse.</p>
        <div class="form-row">
          <div class="form-group">
            <label>Nom ou raison sociale *</label>
            <input type="text" ng-model="parties.importer.name" required>
          </div>
          <div class="form-group">
            <label>Prénom</label>
            <input type="text" ng-model="parties.importer.firstname" ng-disabled="isCompany" placeholder="Prénom">
          </div>
        </div>
        <div class="form-group">
          <label>Adresse (Rue et numéro)</label>
          <input type="text" ng-model="parties.importer.address">
        </div>
        <div class="form-row">
          <div class="form-group">
            <label>NPA *</label>
            <input type="text" ng-model="parties.importer.zip" required>
          </div>
          <div class="form-group">
            <label>Ville *</label>
            <input type="text" ng-model="parties.importer.city" required>
          </div>
        </div>
        <div class="form-group">
          <label>Numéro IDE</label>
          <input type="text" ng-model="parties.importer.ide" placeholder="CHE222251936 pour particulier (défaut)">
        </div>
        <small>Par défaut, le Destinataire et le Déclarant sont identiques à l'Importateur.</small>
      </fieldset>
      <fieldset>
        <legend>Régime Douanier</legend>
        <div class="form-row">
          <div class="form-group">
            <label>Pays d'expédition *</label>
            <select ng-model="declaration.dispatch_country" ng-options="code as name for (code, name) in countries" required>
              <option value="">Sélectionner...</option>
            </select>
          </div>
          <div class="form-group">
            <label>Mode de transport *</label>
            <select ng-model="declaration.transport_mode" ng-options="mode.value as mode.label for mode in transportModes" required></select>
          </div>
        </div>
        <label class="checkbox-label">
          <input type="checkbox" ng-model="declaration.is_relocation"> Importation pour déménagement (Exemption TVA) </label>
        <label class="checkbox-label">
          <input type="checkbox" ng-model="declaration.is_iva_exempt"> Exonéré de l'impôt sur les véhicules automobiles (IVA / RG 660) </label>
      </fieldset>
      <fieldset>
        <legend>Transactions Financières</legend>
        <div class="form-row">
          <div class="form-group">
            <label>Prix d'achat du véhicule *</label>
            <input type="number" step="0.01" ng-model="declaration.purchase_price" ng-change="calculateTotal()" required>
          </div>
          <div class="form-group">
            <label>Monnaie de la facture *</label>
            <select ng-model="declaration.purchase_currency" ng-change="calculateTotal()" ng-options="code as code for (code, name) in currencies" required></select>
          </div>
        </div>
        <p>
          <strong>Coûts additionnels (dans la même devise)</strong>
        </p>
        <div class="form-row">
          <div class="form-group">
            <label>Frais de transport jusqu'à la frontière</label>
            <input type="number" step="0.01" ng-model="declaration.transport_cost" ng-change="calculateTotal()">
          </div>
          <div class="form-group">
            <label>Autres frais (ex: assurance)</label>
            <input type="number" step="0.01" ng-model="declaration.other_costs" ng-change="calculateTotal()">
          </div>
        </div>
        <div class="total-box" ng-if="vatValueCHF > 0">
          <h3>Valeur totale (base de calcul TVA/IVA): ~ {{ vatValueCHF | number:0 }} CHF</h3>
        </div>
      </fieldset>
      <div class="form-actions">
        <button type="button" class="btn btn-secondary" ng-click="prevStep()">← Retour</button>
        <button type="button" class="btn btn-primary" ng-click="generateDeclaration()" ng-disabled="valuationForm.$invalid || generating">
          <span ng-if="!generating">Générer la déclaration ✨</span>
          <span ng-if="generating">Génération en cours...</span>
        </button>
      </div>
    </form>
  </div>
  <div ng-show="step === 4" class="step-content success-content">
    <div ng-if="!showProgress" class="success-icon">✅</div>
    <div ng-if="showProgress" class="progress-section">
      <h2>Soumission en cours...</h2>
      <div class="progress-bar-container">
        <div class="progress-bar" style="width: {{submissionProgress}}%"></div>
      </div>
      <p>{{submissionStep}}... ({{submissionProgress}}%)</p>
    </div>
    <div ng-if="!showProgress">
      <h2>Soumission terminée avec succès !</h2>
      <p>Votre déclaration a été soumise automatiquement au portail e-dec.</p>
      <div class="next-steps-box">
        <h3>📋 Numéro de déclaration: {{declarationNumber}}</h3>
        <p>Les documents ont été téléchargés automatiquement.</p>
        <ul>
          <li>✅ Liste d'importation</li>
          <li>✅ Bulletin de délivrance</li>
        </ul>
      </div>
      <div class="form-actions" style="justify-content: center;">
        <button class="btn btn-secondary" ng-click="resetForm()">Nouvelle déclaration</button>
      </div>
    </div>
  </div>
</div>
----------------------------------------------------
package.json

{
  "name": "edec-vehicles",
  "version": "1.1.0",
  "description": "Application e-dec Véhicules avec AngularJS et ExpressJS",
  "main": "server/server.js",
  "scripts": {
    "start": "node server/server.js",
    "dev": "nodemon server/server.js"
  },
  "dependencies": {
    "axios": "^1.6.2",
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "express-rate-limit": "^7.4.0",
    "express-validator": "^7.1.0",
    "helmet": "^7.1.0",
    "jsonwebtoken": "^9.0.2",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1",
    "mysql2": "^3.6.5",
    "node-cron": "^3.0.3",
    "pdf-parse": "^1.1.1",
    "playwright": "^1.40.1",
    "sharp": "^0.33.4",
    "uuid": "^9.0.1",
    "winston": "^3.11.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  }
}
