Implementation de la notion d'espace d'investissement
This commit is contained in:
1 parent
6b6cad7cd6
commit
4b7df66862
54 files changed
+6782
-288
No files matched your search
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
import { requireInvestisseur } from '../middleware/investisseurScope.js';
|
||||
import { resolveActiveWorkspaceId } from '../middleware/workspaceScope.js';
|
||||
import { adjustSimulForActuals, generateSimulWithReinvestissements } from '../utils/schedule.js';
|
||||
import { assertOwnedCompte } from './fraisOperations.js';
|
||||
|
||||
@@ -13,6 +14,12 @@ const Schema = z.object({
|
||||
investissement_id: z.number().int().positive().nullable().optional(),
|
||||
bonus_plateforme_id: z.number().int().positive().nullable().optional(),
|
||||
bonus_investisseur_id: z.number().int().positive().nullable().optional(),
|
||||
// Lien optionnel vers un deal PE (investissements_pe, 17/09/26) : permet de
|
||||
// rattacher une distribution bonus_plateforme/interets_plateforme à un deal
|
||||
// précis pour calculer son TVPI, quand plusieurs deals partagent une même
|
||||
// plateforme (ex. Fundora) — cf. project_workspaces_transformation.md.
|
||||
// Sans effet sur le type 'normal' (crowdlending).
|
||||
investissement_pe_id: z.number().int().positive().nullable().optional(),
|
||||
date_remb: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
capital: z.number().nonnegative().default(0),
|
||||
cashback: z.number().nonnegative().default(0),
|
||||
@@ -89,7 +96,7 @@ function getTaxIndicatif(body) {
|
||||
* puis en insère un nouveau avec source='auto_remboursement'.
|
||||
* - Sinon : supprime simplement l'ancien retrait lié.
|
||||
*/
|
||||
function syncAutoRetrait(rembId, methode, netRecu, investissementId, dateRemb) {
|
||||
function syncAutoRetrait(rembId, methode, netRecu, investissementId, dateRemb, req) {
|
||||
// Suppression du retrait automatique précédent (s'il y en avait un)
|
||||
db.prepare('DELETE FROM depots_retraits WHERE remboursement_id = ?').run(rembId);
|
||||
|
||||
@@ -100,16 +107,22 @@ function syncAutoRetrait(rembId, methode, netRecu, investissementId, dateRemb) {
|
||||
).get(investissementId);
|
||||
if (!inv) return;
|
||||
|
||||
// Étape 2a du retrofit workspace_id (17/09/26) : le retrait auto généré par un
|
||||
// remboursement hérite du workspace actif au moment du remboursement (même
|
||||
// résolution que les dépôts/retraits manuels, cf. workspaceScope.js).
|
||||
const workspaceId = resolveActiveWorkspaceId(req);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO depots_retraits
|
||||
(investisseur_id, plateforme_id, date_operation, type, montant,
|
||||
libelle, source, remboursement_id)
|
||||
VALUES (?, ?, ?, 'retrait', ?, ?, 'auto_remboursement', ?)
|
||||
libelle, source, remboursement_id, workspace_id)
|
||||
VALUES (?, ?, ?, 'retrait', ?, ?, 'auto_remboursement', ?, ?)
|
||||
`).run(
|
||||
inv.investisseur_id, inv.plateforme_id, dateRemb,
|
||||
netRecu,
|
||||
`Remboursement — ${inv.nom_projet}`,
|
||||
rembId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,6 +214,9 @@ router.get('/', (req, res) => {
|
||||
: `(i.investisseur_id = ?
|
||||
OR (r.investissement_id IS NULL AND r.bonus_investisseur_id = ?))`;
|
||||
const invArg = scopeAll ? req.user.id : req.investisseur.id;
|
||||
// Étape 2c du retrofit workspace_id (17/09/26) : filtre par workspace actif,
|
||||
// même principe que GET /depots-retraits.
|
||||
const workspaceId = resolveActiveWorkspaceId(req);
|
||||
|
||||
const outerConds = [];
|
||||
const outerArgs = [];
|
||||
@@ -228,7 +244,7 @@ router.get('/', (req, res) => {
|
||||
END AS capital_restant_du
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE ${invCond}
|
||||
WHERE ${invCond} AND r.workspace_id = ?
|
||||
)
|
||||
SELECT
|
||||
b.*,
|
||||
@@ -254,7 +270,7 @@ router.get('/', (req, res) => {
|
||||
LEFT JOIN comptes c ON c.id = b.compte_id
|
||||
${outerWhere}
|
||||
ORDER BY b.date_remb DESC, b.id DESC
|
||||
`).all(invArg, invArg, ...outerArgs);
|
||||
`).all(invArg, invArg, workspaceId, ...outerArgs);
|
||||
|
||||
res.json(rows);
|
||||
});
|
||||
@@ -271,18 +287,19 @@ router.post('/', (req, res, next) => {
|
||||
INSERT INTO remboursements
|
||||
(type, investissement_id, date_remb, capital, cashback,
|
||||
interets_bruts_avant_local, taxe_locale, interets_bruts, frais_ttc, prelev_sociaux,
|
||||
prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, methode_remboursement, compte_id)
|
||||
VALUES ('normal',?,?,?,?,?,?,?,?,?,?,?,?,?, 'manuel', ?, ?, ?)
|
||||
prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, methode_remboursement, compte_id, workspace_id)
|
||||
VALUES ('normal',?,?,?,?,?,?,?,?,?,?,?,?,?, 'manuel', ?, ?, ?, ?)
|
||||
`).run(
|
||||
body.investissement_id, body.date_remb, body.capital, body.cashback,
|
||||
body.interets_bruts_avant_local, body.taxe_locale,
|
||||
body.interets_bruts, body.frais_ttc, body.prelev_sociaux, body.prelev_forfaitaire,
|
||||
interets_nets, net_recu, body.statut, body.notes || null, body.methode_remboursement,
|
||||
body.methode_remboursement === 'compte_courant' ? (body.compte_id ?? null) : null,
|
||||
resolveActiveWorkspaceId(req),
|
||||
);
|
||||
syncInvestissementStatut(body.investissement_id);
|
||||
adjustSimulForActuals(db, body.investissement_id);
|
||||
syncAutoRetrait(r.lastInsertRowid, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb);
|
||||
syncAutoRetrait(r.lastInsertRowid, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb, req);
|
||||
{
|
||||
const invPlat = db.prepare('SELECT plateforme_id FROM investissements WHERE id = ?').get(body.investissement_id);
|
||||
syncFraisDistribution(
|
||||
@@ -320,13 +337,14 @@ router.post('/', (req, res, next) => {
|
||||
INSERT INTO remboursements
|
||||
(type, bonus_plateforme_id, bonus_investisseur_id, date_remb,
|
||||
capital, cashback, interets_bruts_avant_local, taxe_locale, interets_bruts, frais_ttc,
|
||||
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, source, notes)
|
||||
VALUES ('interets_plateforme',?,?,?, 0,0,?,?,?,?,?,?, ?,?, ?, 'manuel', ?)
|
||||
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, workspace_id, investissement_pe_id)
|
||||
VALUES ('interets_plateforme',?,?,?, 0,0,?,?,?,?,?,?, ?,?, ?, 'manuel', ?, ?, ?)
|
||||
`).run(
|
||||
body.bonus_plateforme_id, bonusInvestisseurId, body.date_remb,
|
||||
body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, body.frais_ttc,
|
||||
body.prelev_sociaux, body.prelev_forfaitaire,
|
||||
interets_nets, net_recu, body.statut, body.notes || null,
|
||||
resolveActiveWorkspaceId(req), body.investissement_pe_id ?? null,
|
||||
);
|
||||
return res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets, net_recu });
|
||||
}
|
||||
@@ -335,11 +353,12 @@ router.post('/', (req, res, next) => {
|
||||
INSERT INTO remboursements
|
||||
(type, bonus_plateforme_id, bonus_investisseur_id, date_remb,
|
||||
capital, cashback, interets_bruts, prelev_sociaux, prelev_forfaitaire,
|
||||
interets_nets, net_recu, statut, source, notes)
|
||||
VALUES (?,?,?,?, 0,?,0,0,0, 0,?, ?, 'manuel', ?)
|
||||
interets_nets, net_recu, statut, source, notes, workspace_id, investissement_pe_id)
|
||||
VALUES (?,?,?,?, 0,?,0,0,0, 0,?, ?, 'manuel', ?, ?, ?)
|
||||
`).run(
|
||||
body.type, body.bonus_plateforme_id, bonusInvestisseurId, body.date_remb,
|
||||
body.cashback, body.cashback, body.statut, body.notes || null,
|
||||
resolveActiveWorkspaceId(req), body.investissement_pe_id ?? null,
|
||||
);
|
||||
res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets: 0, net_recu: body.cashback });
|
||||
} catch (e) { next(e); }
|
||||
@@ -481,7 +500,7 @@ router.put('/:id', (req, res, next) => {
|
||||
);
|
||||
syncInvestissementStatut(body.investissement_id);
|
||||
adjustSimulForActuals(db, body.investissement_id);
|
||||
syncAutoRetrait(id, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb);
|
||||
syncAutoRetrait(id, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb, req);
|
||||
{
|
||||
const invPlat = db.prepare('SELECT plateforme_id FROM investissements WHERE id = ?').get(body.investissement_id);
|
||||
syncFraisDistribution(
|
||||
@@ -493,20 +512,21 @@ router.put('/:id', (req, res, next) => {
|
||||
db.prepare(`
|
||||
UPDATE remboursements
|
||||
SET date_remb=?, interets_bruts_avant_local=?, taxe_locale=?, interets_bruts=?, frais_ttc=?,
|
||||
prelev_sociaux=?, prelev_forfaitaire=?, interets_nets=?, net_recu=?, statut=?, notes=?
|
||||
prelev_sociaux=?, prelev_forfaitaire=?, interets_nets=?, net_recu=?, statut=?, notes=?,
|
||||
investissement_pe_id=?
|
||||
WHERE id=?
|
||||
`).run(
|
||||
body.date_remb, body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts, body.frais_ttc,
|
||||
body.prelev_sociaux, body.prelev_forfaitaire,
|
||||
interets_nets, net_recu, body.statut, body.notes || null, id,
|
||||
interets_nets, net_recu, body.statut, body.notes || null, body.investissement_pe_id ?? null, id,
|
||||
);
|
||||
} else {
|
||||
const bonusInvestisseurId = body.bonus_investisseur_id ?? existing.bonus_investisseur_id;
|
||||
db.prepare(`
|
||||
UPDATE remboursements
|
||||
SET date_remb=?, cashback=?, net_recu=?, statut=?, notes=?
|
||||
SET date_remb=?, cashback=?, net_recu=?, statut=?, notes=?, investissement_pe_id=?
|
||||
WHERE id=?
|
||||
`).run(body.date_remb, body.cashback, body.cashback, body.statut, body.notes || null, id);
|
||||
`).run(body.date_remb, body.cashback, body.cashback, body.statut, body.notes || null, body.investissement_pe_id ?? null, id);
|
||||
}
|
||||
|
||||
res.json({ id, ...body, interets_nets, net_recu });
|
||||
@@ -526,7 +546,7 @@ router.delete('/:id', (req, res, next) => {
|
||||
`).get(id, req.user.id);
|
||||
if (!existing) throw new HttpError(404, 'Not found');
|
||||
|
||||
syncAutoRetrait(id, 'portefeuille', 0, null, null); // supprime le retrait auto si présent
|
||||
syncAutoRetrait(id, 'portefeuille', 0, null, null, req); // supprime le retrait auto si présent
|
||||
db.prepare('DELETE FROM remboursements WHERE id=?').run(id);
|
||||
if (existing.investissement_id) {
|
||||
syncInvestissementStatut(existing.investissement_id);
|
||||
@@ -545,7 +565,7 @@ router.delete('/', (req, res, next) => {
|
||||
|
||||
const rembs = db.prepare('SELECT id FROM remboursements WHERE investissement_id=?').all(investissement_id);
|
||||
for (const r of rembs) {
|
||||
syncAutoRetrait(r.id, 'portefeuille', 0, null, null);
|
||||
syncAutoRetrait(r.id, 'portefeuille', 0, null, null, req);
|
||||
}
|
||||
db.prepare('DELETE FROM remboursements WHERE investissement_id=?').run(investissement_id);
|
||||
syncInvestissementStatut(investissement_id);
|
||||
|
||||
Reference in new issue
Block a user