Initial commit
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
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 { adjustSimulForActuals, generateSimulWithReinvestissements } from '../utils/schedule.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const Schema = z.object({
|
||||
type: z.enum(['normal', 'bonus_parrainage', 'bonus_plateforme']).default('normal'),
|
||||
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(),
|
||||
date_remb: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
capital: z.number().nonnegative().default(0),
|
||||
cashback: z.number().nonnegative().default(0),
|
||||
interets_bruts_avant_local: z.number().nonnegative().default(0),
|
||||
taxe_locale: z.number().nonnegative().default(0),
|
||||
interets_bruts: z.number().nonnegative().default(0),
|
||||
prelev_sociaux: z.number().nonnegative().default(0),
|
||||
prelev_forfaitaire: z.number().nonnegative().default(0),
|
||||
statut: z.enum(['paye','retard','partiel','impaye']).default('paye'),
|
||||
notes: z.string().optional(),
|
||||
methode_remboursement: z.enum(['portefeuille', 'compte_courant']).default('portefeuille'),
|
||||
compte_id: z.number().int().positive().nullable().optional(),
|
||||
}).refine(data => {
|
||||
if (data.type === 'normal') return !!data.investissement_id;
|
||||
return !!data.bonus_plateforme_id;
|
||||
}, { message: 'investissement_id requis pour type normal ; bonus_plateforme_id requis pour les bonus' });
|
||||
|
||||
/**
|
||||
* isTaxIndicatif = true → plateforme étrangère ou investissement exonéré PEA-PME :
|
||||
* les prélèvements sont stockés à titre indicatif, mais le montant réellement
|
||||
* versé (net_recu) n'en tient pas compte (capital + cashback + interets_bruts).
|
||||
* isTaxIndicatif = false → plateforme flat_tax non exonérée :
|
||||
* les prélèvements sont retenus à la source, net_recu = capital + cashback + interets_nets.
|
||||
*/
|
||||
function computeChamps(body, isTaxIndicatif = false) {
|
||||
const interets_nets = Math.round((body.interets_bruts - body.prelev_sociaux - body.prelev_forfaitaire) * 100) / 100;
|
||||
const net_recu = isTaxIndicatif
|
||||
? Math.round(((body.capital || 0) + (body.cashback || 0) + (body.interets_bruts || 0)) * 100) / 100
|
||||
: Math.round(((body.capital || 0) + (body.cashback || 0) + interets_nets) * 100) / 100;
|
||||
return { interets_nets, net_recu };
|
||||
}
|
||||
|
||||
function getTaxIndicatif(investissementId) {
|
||||
if (!investissementId) return false;
|
||||
const row = db.prepare(`
|
||||
SELECT p.fiscalite, i.fiscalite_override
|
||||
FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE i.id = ?
|
||||
`).get(investissementId);
|
||||
if (!row) return false;
|
||||
return row.fiscalite !== 'flat_tax' || row.fiscalite_override === 'exonere';
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée ou supprime le retrait automatique lié à un remboursement.
|
||||
* Appelé après chaque POST/PUT sur un remboursement normal.
|
||||
* - Si methode === 'compte_courant' : supprime l'ancien retrait lié (s'il existe)
|
||||
* puis en insère un nouveau avec source='auto_remboursement'.
|
||||
* - Sinon : supprime simplement l'ancien retrait lié.
|
||||
*/
|
||||
function syncAutoRetrait(rembId, methode, netRecu, investissementId, dateRemb) {
|
||||
// Suppression du retrait automatique précédent (s'il y en avait un)
|
||||
db.prepare('DELETE FROM depots_retraits WHERE remboursement_id = ?').run(rembId);
|
||||
|
||||
if (methode !== 'compte_courant') return;
|
||||
|
||||
const inv = db.prepare(
|
||||
'SELECT investisseur_id, plateforme_id, nom_projet FROM investissements WHERE id = ?'
|
||||
).get(investissementId);
|
||||
if (!inv) return;
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO depots_retraits
|
||||
(investisseur_id, plateforme_id, date_operation, type, montant,
|
||||
libelle, source, remboursement_id)
|
||||
VALUES (?, ?, ?, 'retrait', ?, ?, 'auto_remboursement', ?)
|
||||
`).run(
|
||||
inv.investisseur_id, inv.plateforme_id, dateRemb,
|
||||
netRecu,
|
||||
`Remboursement — ${inv.nom_projet}`,
|
||||
rembId,
|
||||
);
|
||||
}
|
||||
|
||||
function syncInvestissementStatut(investissement_id) {
|
||||
if (!investissement_id) return;
|
||||
const inv = db.prepare('SELECT montant_investi FROM investissements WHERE id=?').get(investissement_id);
|
||||
if (!inv) return;
|
||||
const { total_capital } = db.prepare(
|
||||
'SELECT COALESCE(SUM(capital), 0) AS total_capital FROM remboursements WHERE investissement_id=?'
|
||||
).get(investissement_id);
|
||||
const { reinvTotal } = db.prepare(
|
||||
'SELECT COALESCE(SUM(montant), 0) AS reinvTotal FROM reinvestissements WHERE investissement_id=?'
|
||||
).get(investissement_id);
|
||||
const capitalTotal = inv.montant_investi + (reinvTotal || 0);
|
||||
const restant = Math.round((capitalTotal - total_capital) * 100) / 100;
|
||||
const newStatut = restant <= 0 ? 'rembourse' : 'en_cours';
|
||||
db.prepare(`
|
||||
UPDATE investissements
|
||||
SET statut = ?, updated_at = datetime('now')
|
||||
WHERE id = ? AND statut IN ('en_cours', 'rembourse')
|
||||
`).run(newStatut, investissement_id);
|
||||
}
|
||||
|
||||
router.use(requireInvestisseur);
|
||||
|
||||
function assertOwnedInvestissement(invId, userId) {
|
||||
const row = db.prepare(`
|
||||
SELECT i.id FROM investissements i
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id AND inv.user_id = ?
|
||||
WHERE i.id = ?
|
||||
`).get(userId, invId);
|
||||
if (!row) throw new HttpError(403, 'Investissement not in scope');
|
||||
}
|
||||
|
||||
function assertOwnedInvestisseur(invId, userId) {
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
|
||||
if (!row) throw new HttpError(403, 'Investisseur not in scope');
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const { from, to, plateforme_id, investissement_id } = req.query;
|
||||
|
||||
const invCond = scopeAll
|
||||
? `(i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)
|
||||
OR (r.investissement_id IS NULL AND r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)))`
|
||||
: `(i.investisseur_id = ?
|
||||
OR (r.investissement_id IS NULL AND r.bonus_investisseur_id = ?))`;
|
||||
const invArg = scopeAll ? req.user.id : req.investisseur.id;
|
||||
|
||||
const outerConds = [];
|
||||
const outerArgs = [];
|
||||
if (from) { outerConds.push('date_remb >= ?'); outerArgs.push(from); }
|
||||
if (to) { outerConds.push('date_remb <= ?'); outerArgs.push(to); }
|
||||
if (plateforme_id) { outerConds.push('plateforme_id = ?'); outerArgs.push(Number(plateforme_id)); }
|
||||
if (investissement_id){ outerConds.push('investissement_id = ?'); outerArgs.push(Number(investissement_id)); }
|
||||
|
||||
const outerWhere = outerConds.length ? `WHERE ${outerConds.join(' AND ')}` : '';
|
||||
|
||||
const rows = db.prepare(`
|
||||
WITH base AS (
|
||||
SELECT r.*,
|
||||
COALESCE(i.plateforme_id, r.bonus_plateforme_id) AS plateforme_id,
|
||||
COALESCE(i.investisseur_id, r.bonus_investisseur_id) AS resolved_investisseur_id,
|
||||
i.nom_projet AS inv_nom_projet,
|
||||
CASE WHEN r.investissement_id IS NOT NULL
|
||||
THEN i.montant_investi - (
|
||||
SELECT COALESCE(SUM(r2.capital), 0)
|
||||
FROM remboursements r2
|
||||
WHERE r2.investissement_id = r.investissement_id
|
||||
AND (r2.date_remb < r.date_remb OR (r2.date_remb = r.date_remb AND r2.id <= r.id))
|
||||
)
|
||||
ELSE 0
|
||||
END AS capital_restant_du
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements i ON i.id = r.investissement_id
|
||||
WHERE ${invCond}
|
||||
)
|
||||
SELECT
|
||||
b.*,
|
||||
CASE b.type
|
||||
WHEN 'normal' THEN b.inv_nom_projet
|
||||
WHEN 'bonus_parrainage' THEN '— Bonus Parrainage'
|
||||
WHEN 'bonus_plateforme' THEN '— Bonus Plateforme'
|
||||
ELSE b.inv_nom_projet
|
||||
END AS nom_projet,
|
||||
p.nom AS plateforme_nom,
|
||||
inv.nom AS investisseur_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
c.nom AS compte_nom
|
||||
FROM base b
|
||||
LEFT JOIN plateformes p ON p.id = b.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
LEFT JOIN investisseurs inv ON inv.id = b.resolved_investisseur_id
|
||||
LEFT JOIN comptes c ON c.id = b.compte_id
|
||||
${outerWhere}
|
||||
ORDER BY b.date_remb DESC, b.id DESC
|
||||
`).all(invArg, invArg, ...outerArgs);
|
||||
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const body = Schema.parse(req.body);
|
||||
const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body.investissement_id));
|
||||
|
||||
if (body.type === 'normal') {
|
||||
assertOwnedInvestissement(body.investissement_id, req.user.id);
|
||||
const r = db.prepare(`
|
||||
INSERT INTO remboursements
|
||||
(type, investissement_id, date_remb, capital, cashback,
|
||||
interets_bruts_avant_local, taxe_locale, interets_bruts, prelev_sociaux,
|
||||
prelev_forfaitaire, interets_nets, net_recu, statut, source, notes, methode_remboursement, compte_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.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,
|
||||
);
|
||||
syncInvestissementStatut(body.investissement_id);
|
||||
adjustSimulForActuals(db, body.investissement_id);
|
||||
syncAutoRetrait(r.lastInsertRowid, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb);
|
||||
// ── Réinvestissement automatique des intérêts ──────────────────────────
|
||||
{
|
||||
const inv = db.prepare(
|
||||
'SELECT i.auto_reinvest, p.fiscalite FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id WHERE i.id = ?'
|
||||
).get(body.investissement_id);
|
||||
if (inv?.auto_reinvest) {
|
||||
// Plateforme française (flat_tax) : utiliser les intérêts nets (prélevés à la source)
|
||||
// Autres plateformes : utiliser les intérêts bruts (rien n'est retenu)
|
||||
const montantAuto = inv.fiscalite === 'flat_tax' ? interets_nets : body.interets_bruts;
|
||||
if (montantAuto > 0) {
|
||||
db.prepare(`
|
||||
INSERT INTO reinvestissements (investissement_id, montant, date_reinvestissement, note, source)
|
||||
VALUES (?, ?, ?, 'Réinvestissement automatique des intérêts', 'auto')
|
||||
`).run(body.investissement_id, montantAuto, body.date_remb);
|
||||
generateSimulWithReinvestissements(db, body.investissement_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets, net_recu });
|
||||
}
|
||||
|
||||
// Bonus (parrainage ou plateforme)
|
||||
const bonusInvestisseurId = body.bonus_investisseur_id ?? req.investisseur.id;
|
||||
assertOwnedInvestisseur(bonusInvestisseurId, req.user.id);
|
||||
const r = db.prepare(`
|
||||
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', ?)
|
||||
`).run(
|
||||
body.type, body.bonus_plateforme_id, bonusInvestisseurId, body.date_remb,
|
||||
body.cashback, body.cashback, body.statut, body.notes || null,
|
||||
);
|
||||
res.status(201).json({ id: r.lastInsertRowid, ...body, interets_nets: 0, net_recu: body.cashback });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* ── Retraitement fiscal en masse ───────────────────────────── */
|
||||
router.post('/reprocess', (req, res, next) => {
|
||||
try {
|
||||
const round2 = v => Math.round(v * 100) / 100;
|
||||
|
||||
const rembs = db.prepare(`
|
||||
SELECT r.id, r.investissement_id, r.capital, r.cashback, r.date_remb,
|
||||
r.interets_bruts_avant_local, r.taxe_locale, r.interets_bruts,
|
||||
p.fiscalite, p.taux_fiscalite_locale,
|
||||
i.fiscalite_override
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
WHERE r.type = 'normal'
|
||||
AND r.investissement_id IS NOT NULL
|
||||
AND inv.user_id = ?
|
||||
`).all(req.user.id);
|
||||
|
||||
const pfuRates = db.prepare('SELECT * FROM taux_pfu').all();
|
||||
const getRates = (dateStr) => {
|
||||
const year = parseInt(dateStr.substring(0, 4), 10);
|
||||
return pfuRates.find(r => r.annee === year) ?? null;
|
||||
};
|
||||
|
||||
const stmtUpdate = db.prepare(`
|
||||
UPDATE remboursements
|
||||
SET taxe_locale=?, interets_bruts=?, prelev_sociaux=?, prelev_forfaitaire=?,
|
||||
interets_nets=?, net_recu=?
|
||||
WHERE id=?
|
||||
`);
|
||||
|
||||
const stmtUpdateRetrait = db.prepare(`
|
||||
UPDATE depots_retraits SET montant=?
|
||||
WHERE remboursement_id=? AND source='auto_remboursement'
|
||||
`);
|
||||
|
||||
let updated = 0;
|
||||
const affectedInvIds = new Set();
|
||||
|
||||
db.transaction(() => {
|
||||
for (const r of rembs) {
|
||||
const rates = getRates(r.date_remb);
|
||||
let taxe_locale = 0;
|
||||
let interets_bruts = r.interets_bruts;
|
||||
let prelev_sociaux = 0;
|
||||
let prelev_forfaitaire = 0;
|
||||
|
||||
if (r.fiscalite === 'avec_fiscalite_locale' && r.taux_fiscalite_locale) {
|
||||
// Base = interets avant taxe locale (fallback sur interets_bruts si colonne vide)
|
||||
const base = (r.interets_bruts_avant_local > 0)
|
||||
? r.interets_bruts_avant_local
|
||||
: r.interets_bruts;
|
||||
taxe_locale = round2(base * r.taux_fiscalite_locale / 100);
|
||||
interets_bruts = round2(base - taxe_locale);
|
||||
}
|
||||
|
||||
// Prélèvements calculés pour TOUTES les plateformes (indicatif pour les non-flat_tax)
|
||||
if (rates) {
|
||||
prelev_sociaux = round2(interets_bruts * rates.prelev_sociaux / 100);
|
||||
prelev_forfaitaire = round2(interets_bruts * rates.impot_revenu / 100);
|
||||
}
|
||||
|
||||
const interets_nets = round2(interets_bruts - prelev_sociaux - prelev_forfaitaire);
|
||||
// Plateforme étrangère ou investissement exonéré : le montant versé n'inclut pas
|
||||
// les prélèvements indicatifs — l'investisseur reçoit le brut intégral.
|
||||
const isTaxIndicatif = r.fiscalite !== 'flat_tax' || r.fiscalite_override === 'exonere';
|
||||
const net_recu = isTaxIndicatif
|
||||
? round2((r.capital || 0) + (r.cashback || 0) + interets_bruts)
|
||||
: round2((r.capital || 0) + (r.cashback || 0) + interets_nets);
|
||||
|
||||
stmtUpdate.run(taxe_locale, interets_bruts, prelev_sociaux, prelev_forfaitaire,
|
||||
interets_nets, net_recu, r.id);
|
||||
stmtUpdateRetrait.run(net_recu, r.id);
|
||||
affectedInvIds.add(r.investissement_id);
|
||||
updated++;
|
||||
}
|
||||
})();
|
||||
|
||||
// Régénérer l'échéancier pour chaque investissement impacté
|
||||
for (const invId of affectedInvIds) {
|
||||
adjustSimulForActuals(db, invId);
|
||||
}
|
||||
|
||||
res.json({ updated });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* ── Modifier un remboursement ──────────────────────────────── */
|
||||
router.put('/:id', (req, res, next) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const existing = db.prepare(`
|
||||
SELECT r.id, r.type, r.investissement_id, r.bonus_plateforme_id, r.bonus_investisseur_id
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements i ON i.id = r.investissement_id
|
||||
LEFT JOIN investisseurs inv ON inv.id = COALESCE(i.investisseur_id, r.bonus_investisseur_id)
|
||||
WHERE r.id = ? AND inv.user_id = ?
|
||||
`).get(id, req.user.id);
|
||||
if (!existing) throw new HttpError(404, 'Not found');
|
||||
|
||||
const body = Schema.parse(req.body);
|
||||
const { interets_nets, net_recu } = computeChamps(body, getTaxIndicatif(body.investissement_id));
|
||||
|
||||
if (body.type === 'normal') {
|
||||
db.prepare(`
|
||||
UPDATE remboursements
|
||||
SET date_remb=?, capital=?, cashback=?,
|
||||
interets_bruts_avant_local=?, taxe_locale=?, interets_bruts=?,
|
||||
prelev_sociaux=?, prelev_forfaitaire=?,
|
||||
interets_nets=?, net_recu=?, statut=?, notes=?, methode_remboursement=?, compte_id=?
|
||||
WHERE id=?
|
||||
`).run(
|
||||
body.date_remb, body.capital, body.cashback,
|
||||
body.interets_bruts_avant_local, body.taxe_locale, body.interets_bruts,
|
||||
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,
|
||||
id,
|
||||
);
|
||||
syncInvestissementStatut(body.investissement_id);
|
||||
adjustSimulForActuals(db, body.investissement_id);
|
||||
syncAutoRetrait(id, body.methode_remboursement, net_recu, body.investissement_id, body.date_remb);
|
||||
} else {
|
||||
const bonusInvestisseurId = body.bonus_investisseur_id ?? existing.bonus_investisseur_id;
|
||||
db.prepare(`
|
||||
UPDATE remboursements
|
||||
SET date_remb=?, cashback=?, net_recu=?, statut=?, notes=?
|
||||
WHERE id=?
|
||||
`).run(body.date_remb, body.cashback, body.cashback, body.statut, body.notes || null, id);
|
||||
}
|
||||
|
||||
res.json({ id, ...body, interets_nets, net_recu });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* ── Supprimer un remboursement ─────────────────────────────── */
|
||||
router.delete('/:id', (req, res, next) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const existing = db.prepare(`
|
||||
SELECT r.id, r.investissement_id
|
||||
FROM remboursements r
|
||||
LEFT JOIN investissements i ON i.id = r.investissement_id
|
||||
LEFT JOIN investisseurs inv ON inv.id = COALESCE(i.investisseur_id, r.bonus_investisseur_id)
|
||||
WHERE r.id = ? AND inv.user_id = ?
|
||||
`).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
|
||||
db.prepare('DELETE FROM remboursements WHERE id=?').run(id);
|
||||
if (existing.investissement_id) {
|
||||
syncInvestissementStatut(existing.investissement_id);
|
||||
adjustSimulForActuals(db, existing.investissement_id);
|
||||
}
|
||||
res.json({ deleted: id });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/* ── Supprimer tous les remboursements d'un investissement ───── */
|
||||
router.delete('/', (req, res, next) => {
|
||||
try {
|
||||
const investissement_id = Number(req.query.investissement_id);
|
||||
if (!investissement_id) throw new HttpError(400, 'investissement_id requis');
|
||||
assertOwnedInvestissement(investissement_id, req.user.id);
|
||||
|
||||
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);
|
||||
}
|
||||
db.prepare('DELETE FROM remboursements WHERE investissement_id=?').run(investissement_id);
|
||||
syncInvestissementStatut(investissement_id);
|
||||
adjustSimulForActuals(db, investissement_id);
|
||||
|
||||
res.json({ deleted: rembs.length });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── Backfill compte_id sur les remboursements ──────────────────────────────
|
||||
// Pour chaque remboursement methode=compte_courant sans compte_id :
|
||||
// 1. Utilise le compte_id de l'investissement lié si disponible
|
||||
// 2. Sinon prend le premier compte de type compte_courant du détenteur
|
||||
router.post('/backfill-comptes', (req, res, next) => {
|
||||
try {
|
||||
const rows = db.prepare(`
|
||||
SELECT r.id AS remb_id, i.compte_id AS inv_compte_id, i.investisseur_id
|
||||
FROM remboursements r
|
||||
JOIN investissements i ON i.id = r.investissement_id
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id AND inv.user_id = ?
|
||||
WHERE r.methode_remboursement = 'compte_courant'
|
||||
AND r.compte_id IS NULL
|
||||
AND r.type = 'normal'
|
||||
`).all(req.user.id);
|
||||
|
||||
let updated = 0;
|
||||
const stmt = db.prepare('UPDATE remboursements SET compte_id=? WHERE id=?');
|
||||
|
||||
for (const row of rows) {
|
||||
let compteId = row.inv_compte_id ?? null;
|
||||
|
||||
// Fallback : premier compte_courant du détenteur
|
||||
if (!compteId) {
|
||||
const compte = db.prepare(
|
||||
"SELECT id FROM comptes WHERE investisseur_id=? AND user_id=? AND type='compte_courant' ORDER BY id LIMIT 1"
|
||||
).get(row.investisseur_id, req.user.id);
|
||||
compteId = compte?.id ?? null;
|
||||
}
|
||||
|
||||
if (compteId) {
|
||||
stmt.run(compteId, row.remb_id);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ updated, total: rows.length });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user