Initial commit
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
import { requireInvestisseur } from '../middleware/investisseurScope.js';
|
||||
import { buildSchedule, adjustSimulForActuals } from '../utils/schedule.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const Schema = z.object({
|
||||
investissement_id: z.number().int().positive(),
|
||||
numero_echeance: z.number().int().positive(),
|
||||
date_prevue: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
capital_prevu: z.number().nonnegative().default(0),
|
||||
interets_prevus: z.number().nonnegative().default(0),
|
||||
total_prevu: z.number().nonnegative().default(0),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
/* GET /api/simul/all — toutes les projections pour l'investisseur actif (ou scope=all) */
|
||||
router.get('/all', (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const rawInvId = req.header('X-Investisseur-Id');
|
||||
const invId = Number(rawInvId);
|
||||
|
||||
let rows;
|
||||
if (scopeAll) {
|
||||
rows = db.prepare(`
|
||||
SELECT s.*, i.plateforme_id, i.investisseur_id
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
WHERE inv.user_id = ?
|
||||
ORDER BY s.date_prevue
|
||||
`).all(userId);
|
||||
} else {
|
||||
if (!invId) return res.status(400).json({ error: 'Missing X-Investisseur-Id' });
|
||||
const ok = db.prepare('SELECT id FROM investisseurs WHERE id=? AND user_id=?').get(invId, userId);
|
||||
if (!ok) return res.status(403).json({ error: 'Investisseur not owned by user' });
|
||||
rows = db.prepare(`
|
||||
SELECT s.*, i.plateforme_id, i.investisseur_id
|
||||
FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE i.investisseur_id = ?
|
||||
ORDER BY s.date_prevue
|
||||
`).all(invId);
|
||||
}
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.use(requireInvestisseur);
|
||||
|
||||
function assertOwnedInvestissement(invId, userId) {
|
||||
const row = db.prepare(`
|
||||
SELECT i.id, i.montant_investi, i.taux_interet, i.duree_mois, i.type_remb,
|
||||
i.freq_interets, i.date_premiere_echeance, i.date_souscription
|
||||
FROM investissements i
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
WHERE i.id = ? AND inv.user_id = ?
|
||||
`).get(invId, userId);
|
||||
if (!row) throw new HttpError(403, 'Investissement not in scope');
|
||||
return row;
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const { investissement_id } = req.query;
|
||||
if (!investissement_id) {
|
||||
return res.status(400).json({ error: 'investissement_id query param required' });
|
||||
}
|
||||
assertOwnedInvestissement(Number(investissement_id), req.user.id);
|
||||
const rows = db.prepare(
|
||||
'SELECT * FROM simul_remboursements WHERE investissement_id=? ORDER BY numero_echeance'
|
||||
).all(investissement_id);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const body = Schema.parse(req.body);
|
||||
assertOwnedInvestissement(body.investissement_id, req.user.id);
|
||||
const r = db.prepare(`
|
||||
INSERT INTO simul_remboursements
|
||||
(investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu, notes)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
`).run(
|
||||
body.investissement_id, body.numero_echeance, body.date_prevue,
|
||||
body.capital_prevu, body.interets_prevus, body.total_prevu, body.notes || null,
|
||||
);
|
||||
res.status(201).json({ id: r.lastInsertRowid, ...body });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/**
|
||||
* Auto-generate the simulated repayment schedule for an investissement
|
||||
* based on its montant, taux, durée, type_remb and freq_interets.
|
||||
*
|
||||
* Body: { investissement_id, replace?: boolean }
|
||||
*/
|
||||
router.post('/generate', (req, res, next) => {
|
||||
try {
|
||||
const { investissement_id, replace = true } = req.body;
|
||||
if (!investissement_id) throw new HttpError(400, 'investissement_id required');
|
||||
const inv = assertOwnedInvestissement(Number(investissement_id), req.user.id);
|
||||
if (!inv.taux_interet || !inv.duree_mois) {
|
||||
throw new HttpError(400, 'Investissement requires taux_interet and duree_mois');
|
||||
}
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
if (replace) {
|
||||
db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(investissement_id);
|
||||
}
|
||||
const start = inv.date_premiere_echeance || inv.date_souscription;
|
||||
const echeances = buildSchedule({
|
||||
montant: inv.montant_investi,
|
||||
taux: inv.taux_interet,
|
||||
duree: inv.duree_mois,
|
||||
type: inv.type_remb || 'in_fine',
|
||||
freq: inv.freq_interets || 'mensuel',
|
||||
startDate: start,
|
||||
});
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO simul_remboursements
|
||||
(investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
`);
|
||||
for (const e of echeances) {
|
||||
stmt.run(investissement_id, e.n, e.date, e.capital, e.interets, e.total);
|
||||
}
|
||||
return echeances.length;
|
||||
});
|
||||
|
||||
const inserted = tx();
|
||||
res.status(201).json({ inserted });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/**
|
||||
* Recalcule l'échéancier en tenant compte des remboursements réels enregistrés
|
||||
* (remboursements anticipés + première période partielle).
|
||||
* Équivalent à ce qui est déclenché automatiquement après chaque saisie de remboursement.
|
||||
*
|
||||
* Body: { investissement_id }
|
||||
*/
|
||||
router.post('/recalculate', (req, res, next) => {
|
||||
try {
|
||||
const { investissement_id } = req.body;
|
||||
if (!investissement_id) throw new HttpError(400, 'investissement_id required');
|
||||
assertOwnedInvestissement(Number(investissement_id), req.user.id);
|
||||
adjustSimulForActuals(db, Number(investissement_id));
|
||||
const rows = db.prepare(
|
||||
'SELECT * FROM simul_remboursements WHERE investissement_id=? ORDER BY numero_echeance'
|
||||
).all(investissement_id);
|
||||
res.json({ recalculated: rows.length, rows });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const owns = db.prepare(`
|
||||
SELECT s.id FROM simul_remboursements s
|
||||
JOIN investissements i ON i.id = s.investissement_id
|
||||
WHERE s.id=? AND i.investisseur_id=?
|
||||
`).get(req.params.id, req.investisseur.id);
|
||||
if (!owns) throw new HttpError(404, 'Not found');
|
||||
db.prepare('DELETE FROM simul_remboursements WHERE id=?').run(req.params.id);
|
||||
res.status(204).end();
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user