diff --git a/backend/src/routes/investissements.js b/backend/src/routes/investissements.js index aa3d4f3..aba2a28 100644 --- a/backend/src/routes/investissements.js +++ b/backend/src/routes/investissements.js @@ -54,7 +54,7 @@ export function detectTypeEvenement(changements) { // table dédiée, déclenche la régénération automatique de l'échéancier futur. const RevisionSchema = z.object({ date_effet: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), - nouveau_taux: z.number().positive().optional(), + nouveau_taux: z.number().nonnegative().optional(), nouvelle_date_cible: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), motif: z.string().trim().min(1, 'Le motif est obligatoire'), }).refine(d => d.nouveau_taux !== undefined || d.nouvelle_date_cible !== undefined, { diff --git a/backend/src/routes/simul.js b/backend/src/routes/simul.js index a6f416e..66231e9 100644 --- a/backend/src/routes/simul.js +++ b/backend/src/routes/simul.js @@ -102,7 +102,9 @@ router.post('/generate', (req, res, next) => { 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) { + // taux_interet peut légitimement valoir 0 (ex. révision suite à un arrêt de production) — + // ne pas confondre avec "non renseigné" (null/undefined). + if (inv.taux_interet == null || !inv.duree_mois) { throw new HttpError(400, 'Investissement requires taux_interet and duree_mois'); } diff --git a/backend/src/utils/schedule.js b/backend/src/utils/schedule.js index 5909e39..49936ff 100644 --- a/backend/src/utils/schedule.js +++ b/backend/src/utils/schedule.js @@ -129,7 +129,9 @@ export function adjustSimulForActuals(db, investissementId) { FROM investissements WHERE id = ? `).get(investissementId); - if (!inv || !inv.taux_interet || !inv.duree_mois) return; + // taux_interet peut légitimement valoir 0 (ex. révision suite à un arrêt de production + // n'engendrant plus d'intérêts) — ne pas confondre avec "non renseigné" (null/undefined). + if (!inv || inv.taux_interet == null || !inv.duree_mois) return; // Réinvestissements triés par date (peuvent être vides) const reinvests = db.prepare( @@ -379,7 +381,8 @@ export function generateSimulWithReinvestissements(db, investissementId) { return; } - if (!inv.taux_interet || !inv.duree_mois) return; + // taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals) + if (inv.taux_interet == null || !inv.duree_mois) return; const startDate = inv.date_debut_simul || inv.date_premiere_echeance || inv.date_souscription; if (!startDate) return; @@ -525,7 +528,8 @@ export function generateSimul(db, inv) { const { id, montant_investi, taux_interet, duree_mois, type_remb, freq_interets, date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois } = inv; - if (!taux_interet || !duree_mois) return; + // taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals) + if (taux_interet == null || !duree_mois) return; // date_debut_simul remplace le point de départ quand le prêt a été restructuré const startDate = date_debut_simul || date_premiere_echeance || date_souscription; diff --git a/frontend/src/pages/InvestissementDetail.jsx b/frontend/src/pages/InvestissementDetail.jsx index 1669635..152a8f4 100644 --- a/frontend/src/pages/InvestissementDetail.jsx +++ b/frontend/src/pages/InvestissementDetail.jsx @@ -661,7 +661,13 @@ export default function InvestissementDetail() { }); setRevisionModalOpen(false); await load(); - } catch (e) { setRevisionErr(e.message); } finally { setRevisionSaving(false); } + } catch (e) { + // Surface le détail des erreurs de validation (Zod) plutôt que le message générique + // "Validation error" renvoyé par errorHandler.js. + const fieldErrors = e.details?.fieldErrors; + const detail = fieldErrors ? Object.values(fieldErrors).flat().filter(Boolean).join(' ') : ''; + setRevisionErr(detail || e.message); + } finally { setRevisionSaving(false); } }; const deleteRevision = async (rid) => { @@ -2603,7 +2609,7 @@ export default function InvestissementDetail() { Traitement en masse des remboursements - {inv.taux_interet && inv.duree_mois ? ( + {inv.taux_interet != null && inv.duree_mois ? (