Lien comptes courants / enveloppes
This commit is contained in:
1 parent
f507163369
commit
9591ae400e
13 files changed
+182
-59
No files matched your search
+91
-9
@@ -2263,12 +2263,25 @@ console.log('[DB] Migrations 2FA OK');
|
|||||||
WHERE i.is_principal = 1
|
WHERE i.is_principal = 1
|
||||||
AND NOT EXISTS (SELECT 1 FROM comptes c WHERE c.investisseur_id = i.id)
|
AND NOT EXISTS (SELECT 1 FROM comptes c WHERE c.investisseur_id = i.id)
|
||||||
`).all();
|
`).all();
|
||||||
|
if (principalsWithoutCompte.length > 0) {
|
||||||
|
// Rattache directement l'enveloppe "Compte courant non rémunéré" et marque le compte comme
|
||||||
|
// compte de règlement par défaut (28/09/26 (suite), fusion Type/Enveloppe demandée par
|
||||||
|
// Olivier). Ce bloc de backfill s'exécute AVANT la création de enveloppes_referentiel plus
|
||||||
|
// bas dans ce fichier — garde d'existence de table pour ne jamais casser un premier
|
||||||
|
// démarrage sur base neuve (où principalsWithoutCompte est de toute façon vide).
|
||||||
|
const enveloppesTableExiste = db.prepare(
|
||||||
|
"SELECT COUNT(*) AS n FROM sqlite_master WHERE type='table' AND name='enveloppes_referentiel'"
|
||||||
|
).get().n > 0;
|
||||||
|
const enveloppeCompteCourantNonRemunere = enveloppesTableExiste
|
||||||
|
? db.prepare("SELECT id FROM enveloppes_referentiel WHERE nom = 'Compte courant non rémunéré'").get()
|
||||||
|
: null;
|
||||||
for (const inv of principalsWithoutCompte) {
|
for (const inv of principalsWithoutCompte) {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
|
'INSERT INTO comptes (user_id, nom, type, enveloppe_id, investisseur_id, compte_reglement_par_defaut) VALUES (?,?,?,?,?,1)'
|
||||||
).run(inv.user_id, `Compte courant — ${inv.nom}`, 'compte_courant', inv.investisseur_id);
|
).run(inv.user_id, `Compte courant — ${inv.nom}`, 'compte_courant', enveloppeCompteCourantNonRemunere?.id ?? null, inv.investisseur_id);
|
||||||
console.log(`[DB] backfill: compte courant créé pour l'investisseur principal #${inv.investisseur_id} (user #${inv.user_id})`);
|
console.log(`[DB] backfill: compte courant créé pour l'investisseur principal #${inv.investisseur_id} (user #${inv.user_id})`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Migration : exclusions de tags hérités du référentiel sur plateformes ──
|
// ── Migration : exclusions de tags hérités du référentiel sur plateformes ──
|
||||||
@@ -4134,13 +4147,13 @@ db.prepare("DELETE FROM supports_referentiel WHERE nom = 'Gestion pilotée / man
|
|||||||
// ── Migration : enveloppe_id sur comptes (Phase 3a, 28/09/26, demande Olivier) ────────────
|
// ── Migration : enveloppe_id sur comptes (Phase 3a, 28/09/26, demande Olivier) ────────────
|
||||||
// Rattache un compte au référentiel enveloppes_referentiel — nullable, non-destructif, sur le
|
// Rattache un compte au référentiel enveloppes_referentiel — nullable, non-destructif, sur le
|
||||||
// même principe que comptes.institution_id (Phase 2, cf. plan de déploiement Institutions
|
// même principe que comptes.institution_id (Phase 2, cf. plan de déploiement Institutions
|
||||||
// financières) : le champ `type` existant (compte_courant/pea_pme) reste en base tel quel et
|
// financières). À l'origine ajouté PUREMENT à côté du champ `type` existant (compte_courant/
|
||||||
// continue de piloter la logique de remboursement/frais déjà en place (de nombreux écrans
|
// pea_pme), qui pilotait alors seul la logique de sélection d'un compte de règlement par défaut
|
||||||
// investissement/remboursement font comptes.find(c => c.type === 'compte_courant') pour
|
// sur les remboursements/frais. Depuis la migration compte_reglement_par_defaut plus bas dans ce
|
||||||
// proposer un compte de règlement par défaut) — enveloppe_id est un enrichissement PUREMENT
|
// fichier (28/09/26 (suite), fusion Type/Enveloppe demandée par Olivier), c'est ce nouveau
|
||||||
// descriptif, ajouté à côté du champ type, jamais en remplacement, pour ne rien casser de cette
|
// booléen dédié qui pilote cette logique — `type` reste en base mais n'est plus lu nulle part.
|
||||||
// logique existante. Restreint aux enveloppes "compte financier" (eligible_compte = 1) côté
|
// Restreint aux enveloppes "compte financier" (eligible_compte = 1) côté sélecteur frontend
|
||||||
// sélecteur frontend uniquement — pas de contrainte CHECK en base, pour rester simple.
|
// uniquement — pas de contrainte CHECK en base, pour rester simple.
|
||||||
{
|
{
|
||||||
const cols = db.prepare('PRAGMA table_info(comptes)').all().map(c => c.name);
|
const cols = db.prepare('PRAGMA table_info(comptes)').all().map(c => c.name);
|
||||||
if (!cols.includes('enveloppe_id')) {
|
if (!cols.includes('enveloppe_id')) {
|
||||||
@@ -4204,4 +4217,73 @@ db.exec(`
|
|||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_emprunts_user ON emprunts(user_id)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_emprunts_user ON emprunts(user_id)');
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_emprunts_inv ON emprunts(investisseur_id)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_emprunts_inv ON emprunts(investisseur_id)');
|
||||||
|
|
||||||
|
// ── Migration : compte_reglement_par_defaut sur comptes (28/09/26 (suite), demande Olivier —
|
||||||
|
// fusion Type/Enveloppe) ──────────────────────────────────────────────────────────────────
|
||||||
|
// Après la Phase 3a, Olivier a demandé d'aller plus loin : éliminer la redondance entre le
|
||||||
|
// champ `type` (compte_courant/pea_pme, 2 valeurs codées en dur) et le rattachement au
|
||||||
|
// référentiel `enveloppe_id` (cf. migration ci-dessus). Exploration du code réel (28/09/26) :
|
||||||
|
// `type` n'a en réalité qu'UN SEUL consommateur métier dans toute l'application — la sélection
|
||||||
|
// d'un compte de règlement par défaut pour un investisseur (comptes.find(c => c.type ===
|
||||||
|
// 'compte_courant'), 7 emplacements frontend + 1 requête backend de rattrapage, tous avec un
|
||||||
|
// repli sur le premier compte sinon). Toutes les autres occurrences de la chaîne
|
||||||
|
// "compte_courant"/"pea_pme" dans le code appartiennent à des champs sans rapport
|
||||||
|
// (methode_remboursement/mode_reglement sur remboursements/frais/investissements,
|
||||||
|
// mode_detention côté Private Equity) et ne sont pas concernées par ce changement.
|
||||||
|
// Remplacement choisi (validé par Olivier via AskUserQuestion, 28/09/26) : un booléen dédié
|
||||||
|
// plutôt qu'une dérivation depuis le nom de l'enveloppe — découplé du choix d'enveloppe, donc
|
||||||
|
// valable même si un jour un compte de règlement par défaut n'est pas un "compte courant" au
|
||||||
|
// sens strict (ex. un livret ou un compte rémunéré fintech). `type` reste en base, non-destructif
|
||||||
|
// comme toujours dans ce chantier, mais n'est plus lu par aucune logique ni affiché/modifiable
|
||||||
|
// dans l'UI (Paramètres > Mes comptes courants) à partir de cette migration.
|
||||||
|
{
|
||||||
|
const cols = db.prepare('PRAGMA table_info(comptes)').all().map(c => c.name);
|
||||||
|
if (!cols.includes('compte_reglement_par_defaut')) {
|
||||||
|
db.exec('ALTER TABLE comptes ADD COLUMN compte_reglement_par_defaut INTEGER NOT NULL DEFAULT 0');
|
||||||
|
// Backfill exécuté une seule fois, dans cette même garde de colonne : reproduit exactement
|
||||||
|
// le comportement actuel (comptes.find(c => c.type === 'compte_courant')) pour chaque
|
||||||
|
// investisseur ayant déjà un compte de type compte_courant — le premier par id, comme le
|
||||||
|
// faisait `.find()` côté frontend. Une édition manuelle ultérieure n'est ensuite jamais
|
||||||
|
// réécrasée par ce bloc (qui ne s'exécute qu'une fois, à la création de la colonne).
|
||||||
|
const investisseurIds = db.prepare(
|
||||||
|
'SELECT DISTINCT investisseur_id FROM comptes WHERE investisseur_id IS NOT NULL'
|
||||||
|
).all().map(r => r.investisseur_id);
|
||||||
|
const findDefault = db.prepare(
|
||||||
|
"SELECT id FROM comptes WHERE investisseur_id = ? AND type = 'compte_courant' ORDER BY id LIMIT 1"
|
||||||
|
);
|
||||||
|
const setDefault = db.prepare('UPDATE comptes SET compte_reglement_par_defaut = 1 WHERE id = ?');
|
||||||
|
db.transaction(() => {
|
||||||
|
for (const invId of investisseurIds) {
|
||||||
|
const row = findDefault.get(invId);
|
||||||
|
if (row) setDefault.run(row.id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Backfill : rattachement rétroactif de l'enveloppe "Compte courant non rémunéré" sur
|
||||||
|
// les comptes existants (28/09/26 (suite), demande Olivier) ─────────────────────────────
|
||||||
|
// La fusion Type/Enveloppe ci-dessus câble l'auto-rattachement enveloppe_id +
|
||||||
|
// compte_reglement_par_defaut UNIQUEMENT sur les 4 points de création d'un compte courant
|
||||||
|
// (inscription, invitation, ajout de membre, seed admin) — jamais en rattrapage sur les
|
||||||
|
// comptes déjà existants avant ce chantier, puisque la règle "compte courant → enveloppe
|
||||||
|
// Compte courant non rémunéré" n'existait pas encore au moment où enveloppe_id a été ajouté
|
||||||
|
// (Phase 3a, purement additif, sans valeur par défaut). Olivier a signalé ce trou après coup
|
||||||
|
// (« est-ce que les utilisateurs existants ont migré le compte courant vers la bonne
|
||||||
|
// enveloppe ? ») — corrigé ici par un backfill toujours exécuté, gardé par
|
||||||
|
// enveloppe_id IS NULL : ne touche donc jamais un compte déjà rattaché, que ce soit
|
||||||
|
// manuellement (comme les comptes d'Olivier) ou par une exécution précédente de ce même
|
||||||
|
// bloc. Ne s'applique qu'aux comptes type='compte_courant' — un PEA-PME reste sans
|
||||||
|
// enveloppe automatique, cohérent avec le fait que seuls les comptes courants sont
|
||||||
|
// auto-créés/auto-rattachés par les 4 points de création listés ci-dessus.
|
||||||
|
{
|
||||||
|
const enveloppeCompteCourantNonRemunere = db.prepare(
|
||||||
|
"SELECT id FROM enveloppes_referentiel WHERE nom = 'Compte courant non rémunéré'"
|
||||||
|
).get();
|
||||||
|
if (enveloppeCompteCourantNonRemunere) {
|
||||||
|
db.prepare(
|
||||||
|
"UPDATE comptes SET enveloppe_id = ? WHERE type = 'compte_courant' AND enveloppe_id IS NULL"
|
||||||
|
).run(enveloppeCompteCourantNonRemunere.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
@@ -55,10 +55,15 @@ router.post('/register', async (req, res, next) => {
|
|||||||
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
|
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
|
||||||
).run(userId, fullName, prenom);
|
).run(userId, fullName, prenom);
|
||||||
|
|
||||||
// Auto-créer un compte courant pour le profil principal
|
// Auto-créer un compte courant pour le profil principal — rattaché directement à
|
||||||
|
// l'enveloppe "Compte courant non rémunéré" et marqué compte de règlement par défaut
|
||||||
|
// (28/09/26 (suite), demande Olivier).
|
||||||
|
const enveloppeCompteCourantNonRemunere = db.prepare(
|
||||||
|
"SELECT id FROM enveloppes_referentiel WHERE nom = 'Compte courant non rémunéré'"
|
||||||
|
).get();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
|
'INSERT INTO comptes (user_id, nom, type, enveloppe_id, investisseur_id, compte_reglement_par_defaut) VALUES (?,?,?,?,?,1)'
|
||||||
).run(userId, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
|
).run(userId, `Compte courant — ${fullName}`, 'compte_courant', enveloppeCompteCourantNonRemunere?.id ?? null, invResult.lastInsertRowid);
|
||||||
|
|
||||||
// Accorde l'accès au workspace socle (crowdlending) à tout nouveau compte
|
// Accorde l'accès au workspace socle (crowdlending) à tout nouveau compte
|
||||||
// — cf. project_workspaces_transformation.md.
|
// — cf. project_workspaces_transformation.md.
|
||||||
|
|||||||
@@ -7,21 +7,33 @@ const router = Router();
|
|||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
nom: z.string().min(1),
|
nom: z.string().min(1),
|
||||||
type: z.enum(['compte_courant', 'pea_pme']).default('compte_courant'),
|
|
||||||
investisseur_id: z.number().int().positive().nullable().optional(),
|
investisseur_id: z.number().int().positive().nullable().optional(),
|
||||||
banque: z.string().nullable().optional(),
|
banque: z.string().nullable().optional(),
|
||||||
institution_id: z.number().int().positive().nullable().optional(),
|
institution_id: z.number().int().positive().nullable().optional(),
|
||||||
// Phase 3a (28/09/26) — rattachement au référentiel enveloppes_referentiel, purement
|
// Phase 3a (28/09/26) — rattachement au référentiel enveloppes_referentiel, purement
|
||||||
// descriptif et non-destructif (cf. commentaire de migration dans db/index.js) : n'a aucune
|
// descriptif et non-destructif (cf. commentaire de migration dans db/index.js). Non contraint
|
||||||
// incidence sur `type`, qui reste le champ utilisé par la logique de remboursement/frais
|
// à eligible_compte=1 côté Zod — le filtrage se fait côté frontend.
|
||||||
// existante. Non contraint à eligible_compte=1 côté Zod — le filtrage se fait côté frontend.
|
|
||||||
enveloppe_id: z.number().int().positive().nullable().optional(),
|
enveloppe_id: z.number().int().positive().nullable().optional(),
|
||||||
exoneration_fiscale: z.enum(['aucune', 'pfnl_5ans']).default('aucune'),
|
exoneration_fiscale: z.enum(['aucune', 'pfnl_5ans']).default('aucune'),
|
||||||
|
// Fusion Type/Enveloppe (28/09/26 (suite), demande Olivier) — remplace l'ancien champ `type`
|
||||||
|
// (compte_courant/pea_pme) comme unique source de vérité pour la sélection d'un compte de
|
||||||
|
// règlement par défaut sur les remboursements/frais (cf. migration compte_reglement_par_defaut
|
||||||
|
// dans db/index.js). Au plus un compte par investisseur ; l'exclusivité est appliquée
|
||||||
|
// ci-dessous via clearOtherDefaults(), pas via une contrainte SQL (investisseur_id nullable).
|
||||||
|
compte_reglement_par_defaut: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function clearOtherDefaults(userId, investisseurId, excludeId) {
|
||||||
|
if (!investisseurId) return;
|
||||||
|
db.prepare(
|
||||||
|
'UPDATE comptes SET compte_reglement_par_defaut = 0 WHERE investisseur_id = ? AND user_id = ? AND id <> ?'
|
||||||
|
).run(investisseurId, userId, excludeId ?? -1);
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT c.id, c.nom, c.type, c.banque, c.institution_id, c.enveloppe_id, c.exoneration_fiscale, c.investisseur_id, c.created_at,
|
SELECT c.id, c.nom, c.banque, c.institution_id, c.enveloppe_id, c.exoneration_fiscale,
|
||||||
|
c.investisseur_id, c.compte_reglement_par_defaut, c.created_at,
|
||||||
inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom,
|
inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom,
|
||||||
inv.type AS investisseur_type, inv.type_fiscal AS investisseur_type_fiscal,
|
inv.type AS investisseur_type, inv.type_fiscal AS investisseur_type_fiscal,
|
||||||
ir.nom AS institution_nom, ir.domiciliation AS institution_domiciliation,
|
ir.nom AS institution_nom, ir.domiciliation AS institution_domiciliation,
|
||||||
@@ -40,20 +52,29 @@ router.get('/', (req, res) => {
|
|||||||
router.post('/', (req, res, next) => {
|
router.post('/', (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = Schema.parse(req.body);
|
const body = Schema.parse(req.body);
|
||||||
|
const defaut = body.compte_reglement_par_defaut ? 1 : 0;
|
||||||
|
const id = db.transaction(() => {
|
||||||
|
if (defaut) clearOtherDefaults(req.user.id, body.investisseur_id);
|
||||||
const r = db.prepare(
|
const r = db.prepare(
|
||||||
'INSERT INTO comptes (user_id, nom, type, banque, institution_id, enveloppe_id, investisseur_id, exoneration_fiscale) VALUES (?,?,?,?,?,?,?,?)'
|
'INSERT INTO comptes (user_id, nom, banque, institution_id, enveloppe_id, investisseur_id, exoneration_fiscale, compte_reglement_par_defaut) VALUES (?,?,?,?,?,?,?,?)'
|
||||||
).run(req.user.id, body.nom, body.type, body.banque || null, body.institution_id ?? null, body.enveloppe_id ?? null, body.investisseur_id ?? null, body.exoneration_fiscale ?? 'aucune');
|
).run(req.user.id, body.nom, body.banque || null, body.institution_id ?? null, body.enveloppe_id ?? null, body.investisseur_id ?? null, body.exoneration_fiscale ?? 'aucune', defaut);
|
||||||
res.status(201).json({ id: r.lastInsertRowid, ...body });
|
return r.lastInsertRowid;
|
||||||
|
})();
|
||||||
|
res.status(201).json({ id, ...body });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:id', (req, res, next) => {
|
router.put('/:id', (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const body = Schema.parse(req.body);
|
const body = Schema.parse(req.body);
|
||||||
const r = db.prepare(
|
const defaut = body.compte_reglement_par_defaut ? 1 : 0;
|
||||||
`UPDATE comptes SET nom=?, type=?, banque=?, institution_id=?, enveloppe_id=?, investisseur_id=?, exoneration_fiscale=?, updated_at=datetime('now') WHERE id=? AND user_id=?`
|
const changes = db.transaction(() => {
|
||||||
).run(body.nom, body.type, body.banque || null, body.institution_id ?? null, body.enveloppe_id ?? null, body.investisseur_id ?? null, body.exoneration_fiscale ?? 'aucune', req.params.id, req.user.id);
|
if (defaut) clearOtherDefaults(req.user.id, body.investisseur_id, Number(req.params.id));
|
||||||
if (r.changes === 0) throw new HttpError(404, 'Not found');
|
return db.prepare(
|
||||||
|
`UPDATE comptes SET nom=?, banque=?, institution_id=?, enveloppe_id=?, investisseur_id=?, exoneration_fiscale=?, compte_reglement_par_defaut=?, updated_at=datetime('now') WHERE id=? AND user_id=?`
|
||||||
|
).run(body.nom, body.banque || null, body.institution_id ?? null, body.enveloppe_id ?? null, body.investisseur_id ?? null, body.exoneration_fiscale ?? 'aucune', defaut, req.params.id, req.user.id).changes;
|
||||||
|
})();
|
||||||
|
if (changes === 0) throw new HttpError(404, 'Not found');
|
||||||
res.json({ id: Number(req.params.id), ...body });
|
res.json({ id: Number(req.params.id), ...body });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ router.get('/comptes-par-investisseur/:investisseur_id', (req, res, next) => {
|
|||||||
.get(invId, req.user.id);
|
.get(invId, req.user.id);
|
||||||
if (!inv) throw new HttpError(403, 'Investisseur non autorisé');
|
if (!inv) throw new HttpError(403, 'Investisseur non autorisé');
|
||||||
const rows = db.prepare(
|
const rows = db.prepare(
|
||||||
'SELECT id, nom, type, banque FROM comptes WHERE investisseur_id = ? AND user_id = ? ORDER BY type, nom'
|
'SELECT id, nom, banque, compte_reglement_par_defaut FROM comptes WHERE investisseur_id = ? AND user_id = ? ORDER BY compte_reglement_par_defaut DESC, nom'
|
||||||
).all(invId, req.user.id);
|
).all(invId, req.user.id);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
|
|||||||
@@ -45,9 +45,15 @@ router.post('/', (req, res, next) => {
|
|||||||
// Auto-créer un compte courant pour ce nouveau profil
|
// Auto-créer un compte courant pour ce nouveau profil
|
||||||
// body.nom contient déjà le nom complet (ex. "Marine CROGUENNEC")
|
// body.nom contient déjà le nom complet (ex. "Marine CROGUENNEC")
|
||||||
const label = `Compte courant — ${body.nom}`;
|
const label = `Compte courant — ${body.nom}`;
|
||||||
|
// Rattache directement l'enveloppe "Compte courant non rémunéré" et marque ce compte comme
|
||||||
|
// compte de règlement par défaut — plus besoin de le faire à la main ensuite dans
|
||||||
|
// Paramètres > Mes comptes courants (28/09/26 (suite), demande Olivier).
|
||||||
|
const enveloppeCompteCourantNonRemunere = db.prepare(
|
||||||
|
"SELECT id FROM enveloppes_referentiel WHERE nom = 'Compte courant non rémunéré'"
|
||||||
|
).get();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
|
'INSERT INTO comptes (user_id, nom, type, enveloppe_id, investisseur_id, compte_reglement_par_defaut) VALUES (?,?,?,?,?,1)'
|
||||||
).run(req.user.id, label, 'compte_courant', invId);
|
).run(req.user.id, label, 'compte_courant', enveloppeCompteCourantNonRemunere?.id ?? null, invId);
|
||||||
|
|
||||||
res.status(201).json({ id: invId, ...body });
|
res.status(201).json({ id: invId, ...body });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
|
|||||||
@@ -194,9 +194,14 @@ router.post('/:token/register', async (req, res, next) => {
|
|||||||
const invResult = db.prepare(
|
const invResult = db.prepare(
|
||||||
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
|
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
|
||||||
).run(pendingUser.id, fullName, prenom);
|
).run(pendingUser.id, fullName, prenom);
|
||||||
|
// Rattache directement l'enveloppe "Compte courant non rémunéré" et marque ce compte comme
|
||||||
|
// compte de règlement par défaut (28/09/26 (suite), demande Olivier).
|
||||||
|
const enveloppeCompteCourantNonRemunere = db.prepare(
|
||||||
|
"SELECT id FROM enveloppes_referentiel WHERE nom = 'Compte courant non rémunéré'"
|
||||||
|
).get();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
|
'INSERT INTO comptes (user_id, nom, type, enveloppe_id, investisseur_id, compte_reglement_par_defaut) VALUES (?,?,?,?,?,1)'
|
||||||
).run(pendingUser.id, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
|
).run(pendingUser.id, `Compte courant — ${fullName}`, 'compte_courant', enveloppeCompteCourantNonRemunere?.id ?? null, invResult.lastInsertRowid);
|
||||||
|
|
||||||
// Accorde l'accès au workspace socle (crowdlending) à tout nouveau compte
|
// Accorde l'accès au workspace socle (crowdlending) à tout nouveau compte
|
||||||
// — cf. project_workspaces_transformation.md.
|
// — cf. project_workspaces_transformation.md.
|
||||||
|
|||||||
@@ -612,7 +612,7 @@ router.post('/backfill-comptes', (req, res, next) => {
|
|||||||
// Fallback : premier compte_courant du détenteur
|
// Fallback : premier compte_courant du détenteur
|
||||||
if (!compteId) {
|
if (!compteId) {
|
||||||
const compte = db.prepare(
|
const compte = db.prepare(
|
||||||
"SELECT id FROM comptes WHERE investisseur_id=? AND user_id=? AND type='compte_courant' ORDER BY id LIMIT 1"
|
"SELECT id FROM comptes WHERE investisseur_id=? AND user_id=? AND compte_reglement_par_defaut=1 ORDER BY id LIMIT 1"
|
||||||
).get(row.investisseur_id, req.user.id);
|
).get(row.investisseur_id, req.user.id);
|
||||||
compteId = compte?.id ?? null;
|
compteId = compte?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default function InvestissementFormModal({
|
|||||||
|
|
||||||
const handleMethodeChange = (methode) => {
|
const handleMethodeChange = (methode) => {
|
||||||
const defaultCompte = methode === 'compte_courant'
|
const defaultCompte = methode === 'compte_courant'
|
||||||
? (comptesInvestisseur.find(c => c.type === 'compte_courant') ?? comptesInvestisseur[0])
|
? (comptesInvestisseur.find(c => c.compte_reglement_par_defaut) ?? comptesInvestisseur[0])
|
||||||
: null;
|
: null;
|
||||||
onFormPatch({ methode_remboursement: methode, nom_compte_courant: defaultCompte?.nom || '', compte_id: String(defaultCompte?.id || '') });
|
onFormPatch({ methode_remboursement: methode, nom_compte_courant: defaultCompte?.nom || '', compte_id: String(defaultCompte?.id || '') });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function RembFormModal({
|
|||||||
const modalTitle = title ?? (editingId ? 'Modifier le remboursement' : (locked ? 'Saisir le remboursement' : 'Nouveau remboursement'));
|
const modalTitle = title ?? (editingId ? 'Modifier le remboursement' : (locked ? 'Saisir le remboursement' : 'Nouveau remboursement'));
|
||||||
|
|
||||||
const handleMethodeChange = (m) => {
|
const handleMethodeChange = (m) => {
|
||||||
const def = m === 'compte_courant' ? (comptesInvestisseur?.find(c => c.type === 'compte_courant') ?? comptesInvestisseur?.[0]) : null;
|
const def = m === 'compte_courant' ? (comptesInvestisseur?.find(c => c.compte_reglement_par_defaut) ?? comptesInvestisseur?.[0]) : null;
|
||||||
onFormPatch({ methode_remboursement: m, compte_id: def ? String(def.id) : '' });
|
onFormPatch({ methode_remboursement: m, compte_id: def ? String(def.id) : '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export default function RembFormModal({
|
|||||||
// 'compte_courant' — même pattern que handleFraisReglementChange côté investissement.
|
// 'compte_courant' — même pattern que handleFraisReglementChange côté investissement.
|
||||||
const handleFraisModeChange = (mode) => {
|
const handleFraisModeChange = (mode) => {
|
||||||
onFieldChange('frais_mode_reglement', mode);
|
onFieldChange('frais_mode_reglement', mode);
|
||||||
const def = mode === 'compte_courant' ? (comptesInvestisseur?.find(c => c.type === 'compte_courant') ?? comptesInvestisseur?.[0]) : null;
|
const def = mode === 'compte_courant' ? (comptesInvestisseur?.find(c => c.compte_reglement_par_defaut) ?? comptesInvestisseur?.[0]) : null;
|
||||||
onFormPatch({ frais_compte_id: def ? String(def.id) : '' });
|
onFormPatch({ frais_compte_id: def ? String(def.id) : '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function InvestissementDetail() {
|
|||||||
.then(comptes => {
|
.then(comptes => {
|
||||||
setComptesInvestisseur(comptes);
|
setComptesInvestisseur(comptes);
|
||||||
if (form.methode_remboursement === 'compte_courant' && !form.compte_id) {
|
if (form.methode_remboursement === 'compte_courant' && !form.compte_id) {
|
||||||
const def = comptes.find(c => c.type === 'compte_courant') ?? comptes[0];
|
const def = comptes.find(c => c.compte_reglement_par_defaut) ?? comptes[0];
|
||||||
if (def) setForm(f => ({ ...f, compte_id: String(def.id), nom_compte_courant: def.nom }));
|
if (def) setForm(f => ({ ...f, compte_id: String(def.id), nom_compte_courant: def.nom }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -148,7 +148,7 @@ export default function InvestissementDetail() {
|
|||||||
.then(comptes => {
|
.then(comptes => {
|
||||||
setComptesRembInvestisseur(comptes);
|
setComptesRembInvestisseur(comptes);
|
||||||
if (rembForm.methode_remboursement === 'compte_courant' && !rembForm.compte_id) {
|
if (rembForm.methode_remboursement === 'compte_courant' && !rembForm.compte_id) {
|
||||||
const def = comptes.find(c => c.type === 'compte_courant') ?? comptes[0];
|
const def = comptes.find(c => c.compte_reglement_par_defaut) ?? comptes[0];
|
||||||
if (def) setRembForm(f => ({ ...f, compte_id: String(def.id) }));
|
if (def) setRembForm(f => ({ ...f, compte_id: String(def.id) }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default function Investissements() {
|
|||||||
.then(comptes => {
|
.then(comptes => {
|
||||||
setComptesInvestisseur(comptes);
|
setComptesInvestisseur(comptes);
|
||||||
if (form.methode_remboursement === 'compte_courant' && !form.compte_id) {
|
if (form.methode_remboursement === 'compte_courant' && !form.compte_id) {
|
||||||
const def = comptes.find(c => c.type === 'compte_courant') ?? comptes[0];
|
const def = comptes.find(c => c.compte_reglement_par_defaut) ?? comptes[0];
|
||||||
if (def) setForm(f => ({ ...f, compte_id: String(def.id), nom_compte_courant: def.nom }));
|
if (def) setForm(f => ({ ...f, compte_id: String(def.id), nom_compte_courant: def.nom }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ export default function Remboursements() {
|
|||||||
if (next.methode_remboursement === 'compte_courant') {
|
if (next.methode_remboursement === 'compte_courant') {
|
||||||
// Priorité : compte lié à l'investissement, sinon premier compte_courant
|
// Priorité : compte lié à l'investissement, sinon premier compte_courant
|
||||||
const inherited = inv.compte_id ? comptes.find(c => c.id === inv.compte_id) : null;
|
const inherited = inv.compte_id ? comptes.find(c => c.id === inv.compte_id) : null;
|
||||||
const def = inherited ?? comptes.find(c => c.type === 'compte_courant') ?? comptes[0];
|
const def = inherited ?? comptes.find(c => c.compte_reglement_par_defaut) ?? comptes[0];
|
||||||
if (def) setForm(f => ({ ...f, compte_id: String(def.id) }));
|
if (def) setForm(f => ({ ...f, compte_id: String(def.id) }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ const EXONERATION_LABELS = {
|
|||||||
aucune: 'Aucune',
|
aucune: 'Aucune',
|
||||||
pfnl_5ans: 'PFnl 5 ans',
|
pfnl_5ans: 'PFnl 5 ans',
|
||||||
};
|
};
|
||||||
const TYPE_COMPTE_LABELS = {
|
const EMPTY_COMPTE = { nom: '', investisseur_id: null, institution_id: null, banque: '', enveloppe_id: null, exoneration_fiscale: 'aucune', compte_reglement_par_defaut: false };
|
||||||
compte_courant: 'Compte courant',
|
|
||||||
pea_pme: 'PEA-PME',
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_COMPTE = { nom: '', type: 'compte_courant', investisseur_id: null, institution_id: null, banque: '', enveloppe_id: null, exoneration_fiscale: 'aucune' };
|
|
||||||
|
|
||||||
// Référentiel institutions financières (Phase 2, 26/09/26) — logos servis au même endroit
|
// Référentiel institutions financières (Phase 2, 26/09/26) — logos servis au même endroit
|
||||||
// que ceux des plateformes (/api/logos/:filename), cf. institutionsReferentiel.js.
|
// que ceux des plateformes (/api/logos/:filename), cf. institutionsReferentiel.js.
|
||||||
@@ -45,14 +40,6 @@ function CompteFormFields({ state, setter, investisseurs, institutions, envelopp
|
|||||||
onChange={e => setter({ ...state, nom: e.target.value })}
|
onChange={e => setter({ ...state, nom: e.target.value })}
|
||||||
placeholder="ex. Compte courant BNP" />
|
placeholder="ex. Compte courant BNP" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
|
||||||
<div>
|
|
||||||
<label>Type *</label>
|
|
||||||
<select value={state.type} onChange={e => setter({ ...state, type: e.target.value })}>
|
|
||||||
<option value="compte_courant">Compte courant</option>
|
|
||||||
<option value="pea_pme">PEA-PME</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label>Banque</label>
|
<label>Banque</label>
|
||||||
<select
|
<select
|
||||||
@@ -70,7 +57,6 @@ function CompteFormFields({ state, setter, investisseurs, institutions, envelopp
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{isAutre && (
|
{isAutre && (
|
||||||
<div>
|
<div>
|
||||||
<label>Nom de la banque (texte libre)</label>
|
<label>Nom de la banque (texte libre)</label>
|
||||||
@@ -91,7 +77,7 @@ function CompteFormFields({ state, setter, investisseurs, institutions, envelopp
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p style={{ margin: '4px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>
|
<p style={{ margin: '4px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>
|
||||||
Rattachement au référentiel (Admin > Enveloppes & supports) — informatif, sans effet sur le champ Type ci-dessus.
|
Rattachement au référentiel (Admin > Enveloppes & supports).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -104,6 +90,18 @@ function CompteFormFields({ state, setter, investisseurs, institutions, envelopp
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={!!state.compte_reglement_par_defaut} style={{ width: 'auto' }}
|
||||||
|
onChange={e => setter({ ...state, compte_reglement_par_defaut: e.target.checked })} />
|
||||||
|
Compte de règlement par défaut
|
||||||
|
</label>
|
||||||
|
<p style={{ margin: '2px 0 0', fontSize: 11, color: 'var(--text-muted)' }}>
|
||||||
|
Utilisé pour pré-remplir automatiquement le compte de règlement sur les remboursements
|
||||||
|
et frais de ce détenteur. Un seul compte par défaut par détenteur (28/09/26 — remplace
|
||||||
|
l'ancien champ Type).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Exonération fiscale</label>
|
<label>Exonération fiscale</label>
|
||||||
<select value={state.exoneration_fiscale ?? 'aucune'}
|
<select value={state.exoneration_fiscale ?? 'aucune'}
|
||||||
@@ -153,12 +151,12 @@ export default function ComptesSection() {
|
|||||||
try {
|
try {
|
||||||
await api.post('/comptes', {
|
await api.post('/comptes', {
|
||||||
nom: newCompte.nom,
|
nom: newCompte.nom,
|
||||||
type: newCompte.type,
|
|
||||||
banque: newCompte.banque || null,
|
banque: newCompte.banque || null,
|
||||||
institution_id: newCompte.institution_id ?? null,
|
institution_id: newCompte.institution_id ?? null,
|
||||||
enveloppe_id: newCompte.enveloppe_id ?? null,
|
enveloppe_id: newCompte.enveloppe_id ?? null,
|
||||||
investisseur_id: newCompte.investisseur_id ? Number(newCompte.investisseur_id) : null,
|
investisseur_id: newCompte.investisseur_id ? Number(newCompte.investisseur_id) : null,
|
||||||
exoneration_fiscale: newCompte.exoneration_fiscale ?? 'aucune',
|
exoneration_fiscale: newCompte.exoneration_fiscale ?? 'aucune',
|
||||||
|
compte_reglement_par_defaut: !!newCompte.compte_reglement_par_defaut,
|
||||||
});
|
});
|
||||||
setShowNew(false); setNewCompte(EMPTY_COMPTE); reload();
|
setShowNew(false); setNewCompte(EMPTY_COMPTE); reload();
|
||||||
} catch (ex) { setErr(ex.message); }
|
} catch (ex) { setErr(ex.message); }
|
||||||
@@ -169,12 +167,12 @@ export default function ComptesSection() {
|
|||||||
try {
|
try {
|
||||||
await api.put(`/comptes/${editCompte.id}`, {
|
await api.put(`/comptes/${editCompte.id}`, {
|
||||||
nom: editCompte.nom,
|
nom: editCompte.nom,
|
||||||
type: editCompte.type,
|
|
||||||
banque: editCompte.banque || null,
|
banque: editCompte.banque || null,
|
||||||
institution_id: editCompte.institution_id ?? null,
|
institution_id: editCompte.institution_id ?? null,
|
||||||
enveloppe_id: editCompte.enveloppe_id ?? null,
|
enveloppe_id: editCompte.enveloppe_id ?? null,
|
||||||
investisseur_id: editCompte.investisseur_id ? Number(editCompte.investisseur_id) : null,
|
investisseur_id: editCompte.investisseur_id ? Number(editCompte.investisseur_id) : null,
|
||||||
exoneration_fiscale: editCompte.exoneration_fiscale ?? 'aucune',
|
exoneration_fiscale: editCompte.exoneration_fiscale ?? 'aucune',
|
||||||
|
compte_reglement_par_defaut: !!editCompte.compte_reglement_par_defaut,
|
||||||
});
|
});
|
||||||
setEditCompte(null); reload();
|
setEditCompte(null); reload();
|
||||||
} catch (ex) { setErr(ex.message); }
|
} catch (ex) { setErr(ex.message); }
|
||||||
@@ -182,7 +180,7 @@ export default function ComptesSection() {
|
|||||||
|
|
||||||
const openEdit = (c) => {
|
const openEdit = (c) => {
|
||||||
setErr(null);
|
setErr(null);
|
||||||
setEditCompte({ id: c.id, nom: c.nom, type: c.type, banque: c.banque || '', institution_id: c.institution_id ?? null, enveloppe_id: c.enveloppe_id ?? null, investisseur_id: c.investisseur_id ?? null, exoneration_fiscale: c.exoneration_fiscale ?? 'aucune' });
|
setEditCompte({ id: c.id, nom: c.nom, banque: c.banque || '', institution_id: c.institution_id ?? null, enveloppe_id: c.enveloppe_id ?? null, investisseur_id: c.investisseur_id ?? null, exoneration_fiscale: c.exoneration_fiscale ?? 'aucune', compte_reglement_par_defaut: !!c.compte_reglement_par_defaut });
|
||||||
};
|
};
|
||||||
|
|
||||||
const del = (c) => {
|
const del = (c) => {
|
||||||
@@ -219,11 +217,11 @@ export default function ComptesSection() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nom</th>
|
<th>Nom</th>
|
||||||
<th style={{ width: '13%' }}>Type</th>
|
|
||||||
{multiDetenteur && <th style={{ width: '18%' }}>Détenteur</th>}
|
{multiDetenteur && <th style={{ width: '18%' }}>Détenteur</th>}
|
||||||
<th style={{ width: '14%' }}>Banque</th>
|
<th style={{ width: '14%' }}>Banque</th>
|
||||||
<th style={{ width: '16%' }}>Enveloppe</th>
|
<th style={{ width: '16%' }}>Enveloppe</th>
|
||||||
<th style={{ width: '10%' }}>Exonération</th>
|
<th style={{ width: '11%' }}>Défaut</th>
|
||||||
|
<th style={{ width: '9%' }}>Exonération</th>
|
||||||
<th style={{ width: 80 }} />
|
<th style={{ width: 80 }} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -238,7 +236,6 @@ export default function ComptesSection() {
|
|||||||
return (
|
return (
|
||||||
<tr key={c.id}>
|
<tr key={c.id}>
|
||||||
<td style={{ fontWeight: 500 }}>{c.nom}</td>
|
<td style={{ fontWeight: 500 }}>{c.nom}</td>
|
||||||
<td><span className="badge">{TYPE_COMPTE_LABELS[c.type] ?? c.type}</span></td>
|
|
||||||
{multiDetenteur && <td>{inv ? memberLabel(inv) : <span className="text-muted">—</span>}</td>}
|
{multiDetenteur && <td>{inv ? memberLabel(inv) : <span className="text-muted">—</span>}</td>}
|
||||||
<td>
|
<td>
|
||||||
{c.institution_id ? (
|
{c.institution_id ? (
|
||||||
@@ -252,6 +249,13 @@ export default function ComptesSection() {
|
|||||||
) : (c.banque || <span className="text-muted">—</span>)}
|
) : (c.banque || <span className="text-muted">—</span>)}
|
||||||
</td>
|
</td>
|
||||||
<td>{c.enveloppe_nom || <span className="text-muted">—</span>}</td>
|
<td>{c.enveloppe_nom || <span className="text-muted">—</span>}</td>
|
||||||
|
<td>
|
||||||
|
{c.compte_reglement_par_defaut ? (
|
||||||
|
<span style={{ color: 'var(--primary)', fontWeight: 600, fontSize: 'var(--fs-sm)' }}>✓ Défaut</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{c.exoneration_fiscale && c.exoneration_fiscale !== 'aucune' ? (
|
{c.exoneration_fiscale && c.exoneration_fiscale !== 'aucune' ? (
|
||||||
<span
|
<span
|
||||||
|
|||||||
Reference in new issue
Block a user