import { Router } from 'express'; import multer from 'multer'; import db from '../db/index.js'; import { createZip, readZip } from '../utils/zip.js'; const router = Router(); const zipStorage = multer.memoryStorage(); const zipUpload = multer({ storage: zipStorage, limits: { fileSize: 5 * 1024 * 1024 } }); /* GET /api/pfu */ router.get('/', (req, res) => { const rows = db.prepare('SELECT * FROM taux_pfu ORDER BY annee ASC').all(); res.json(rows); }); /* GET /api/pfu/export-zip — export ZIP fiscal (PFU + crédit d'impôt) */ router.get('/export-zip', (req, res, next) => { try { const pfu = db.prepare('SELECT * FROM taux_pfu ORDER BY annee ASC').all(); const tci = db.prepare('SELECT * FROM taux_credit_impot ORDER BY nom_pays ASC').all(); const manifest = { version: '1.0', app: 'crowdlending', exported_at: new Date().toISOString(), type: 'fiscal-referentiel', }; const pfuData = pfu.map(r => ({ annee: r.annee, impot_revenu: r.impot_revenu, csg: r.csg, crds: r.crds, solidarite: r.solidarite, })); const tciData = tci.map(r => ({ nom_pays: r.nom_pays, code_pays: r.code_pays, div_taux: r.div_taux, div_taux_alt: r.div_taux_alt, div_taux_alt_label: r.div_taux_alt_label, div_exclusif_residence: r.div_exclusif_residence, int_taux: r.int_taux, int_taux_alt: r.int_taux_alt, int_taux_alt_label: r.int_taux_alt_label, int_exclusif_residence: r.int_exclusif_residence, notice: r.notice, statut_convention: r.statut_convention, date_suspension: r.date_suspension, ref_boi: r.ref_boi, })); const entries = [ { name: 'manifest.json', data: JSON.stringify(manifest, null, 2) }, { name: 'pfu.json', data: JSON.stringify(pfuData, null, 2) }, { name: 'taux_credit_impot.json', data: JSON.stringify(tciData, null, 2) }, ]; const zipBuf = createZip(entries); const slug = 'fiscal-referentiel-' + new Date().toISOString().slice(0, 10); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${slug}.zip"`); res.send(zipBuf); } catch (e) { next(e); } }); /* POST /api/pfu/import-zip — import ZIP fiscal (PFU + crédit d'impôt) */ router.post('/import-zip', zipUpload.single('file'), (req, res, next) => { try { if (!req.file) return res.status(400).json({ error: 'Fichier ZIP manquant' }); const zipEntries = readZip(req.file.buffer); const pfuEntry = zipEntries.find(e => e.name === 'pfu.json'); const tciEntry = zipEntries.find(e => e.name === 'taux_credit_impot.json'); if (!pfuEntry && !tciEntry) { return res.status(400).json({ error: 'ZIP invalide : aucun fichier reconnu' }); } let pfuCreated = 0, pfuUpdated = 0; let tciCreated = 0, tciUpdated = 0; const tx = db.transaction(() => { if (pfuEntry) { const rows = JSON.parse(pfuEntry.data.toString('utf8')); for (const r of rows) { if (!r.annee) continue; const ps = Number(r.csg || 0) + Number(r.crds || 0) + Number(r.solidarite || 0); const total = Number(r.impot_revenu || 0) + ps; const ex = db.prepare('SELECT id FROM taux_pfu WHERE annee = ?').get(Number(r.annee)); if (ex) { db.prepare( `UPDATE taux_pfu SET pfu_total=?, impot_revenu=?, prelev_sociaux=?, csg=?, crds=?, solidarite=?, updated_at=datetime('now') WHERE id=?` ).run(total, Number(r.impot_revenu), ps, Number(r.csg), Number(r.crds), Number(r.solidarite), ex.id); pfuUpdated++; } else { db.prepare( 'INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux, csg, crds, solidarite) VALUES (?,?,?,?,?,?,?)' ).run(Number(r.annee), total, Number(r.impot_revenu), ps, Number(r.csg), Number(r.crds), Number(r.solidarite)); pfuCreated++; } } } if (tciEntry) { const rows = JSON.parse(tciEntry.data.toString('utf8')); for (const r of rows) { if (!r.nom_pays) continue; const ex = db.prepare( 'SELECT id FROM taux_credit_impot WHERE LOWER(nom_pays) = LOWER(?) OR (code_pays IS NOT NULL AND code_pays = ?)' ).get(r.nom_pays, r.code_pays || ''); const vals = [ r.nom_pays, r.code_pays || null, r.div_taux ?? null, r.div_taux_alt ?? null, r.div_taux_alt_label || null, r.div_exclusif_residence ? 1 : 0, r.int_taux ?? null, r.int_taux_alt ?? null, r.int_taux_alt_label || null, r.int_exclusif_residence ? 1 : 0, r.notice || null, r.statut_convention || 'active', r.date_suspension || null, r.ref_boi || null, ]; if (ex) { db.prepare( `UPDATE taux_credit_impot SET nom_pays=?, code_pays=?, div_taux=?, div_taux_alt=?, div_taux_alt_label=?, div_exclusif_residence=?, int_taux=?, int_taux_alt=?, int_taux_alt_label=?, int_exclusif_residence=?, notice=?, statut_convention=?, date_suspension=?, ref_boi=?, updated_at=datetime('now') WHERE id=?` ).run(...vals, ex.id); tciUpdated++; } else { db.prepare( `INSERT INTO taux_credit_impot (nom_pays, code_pays, div_taux, div_taux_alt, div_taux_alt_label, div_exclusif_residence, int_taux, int_taux_alt, int_taux_alt_label, int_exclusif_residence, notice, statut_convention, date_suspension, ref_boi) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)` ).run(...vals); tciCreated++; } } } }); tx(); res.json({ ok: true, pfu: { created: pfuCreated, updated: pfuUpdated }, tci: { created: tciCreated, updated: tciUpdated }, }); } catch (e) { next(e); } }); /* POST /api/pfu */ router.post('/', (req, res) => { const { annee, impot_revenu, csg, crds, solidarite } = req.body; if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) { return res.status(400).json({ error: 'Champs requis : annee, impot_revenu, csg, crds, solidarite' }); } const prelev_sociaux = Number(csg) + Number(crds) + Number(solidarite); const pfu_total = Number(impot_revenu) + prelev_sociaux; try { const result = db.prepare( 'INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux, csg, crds, solidarite) VALUES (?, ?, ?, ?, ?, ?, ?)' ).run(Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux, Number(csg), Number(crds), Number(solidarite)); res.status(201).json(db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(result.lastInsertRowid)); } catch (e) { if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: `L'année ${annee} existe déjà.` }); throw e; } }); /* PUT /api/pfu/:id */ router.put('/:id', (req, res) => { const { annee, impot_revenu, csg, crds, solidarite } = req.body; if (!annee || impot_revenu == null || csg == null || crds == null || solidarite == null) { return res.status(400).json({ error: 'Champs requis : annee, impot_revenu, csg, crds, solidarite' }); } const prelev_sociaux = Number(csg) + Number(crds) + Number(solidarite); const pfu_total = Number(impot_revenu) + prelev_sociaux; try { db.prepare( `UPDATE taux_pfu SET annee=?, pfu_total=?, impot_revenu=?, prelev_sociaux=?, csg=?, crds=?, solidarite=?, updated_at=datetime('now') WHERE id=?` ).run(Number(annee), pfu_total, Number(impot_revenu), prelev_sociaux, Number(csg), Number(crds), Number(solidarite), Number(req.params.id)); const row = db.prepare('SELECT * FROM taux_pfu WHERE id = ?').get(Number(req.params.id)); if (!row) return res.status(404).json({ error: 'Introuvable' }); res.json(row); } catch (e) { if (e.message?.includes('UNIQUE')) return res.status(409).json({ error: `L'année ${annee} existe déjà.` }); throw e; } }); /* DELETE /api/pfu/:id */ router.delete('/:id', (req, res) => { db.prepare('DELETE FROM taux_pfu WHERE id = ?').run(Number(req.params.id)); res.status(204).end(); }); export default router;