@mostajs/wagon-engine 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
3
3
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
4
  Format : Keep a Changelog ; versions : SemVer.
5
5
 
6
+ ## [0.7.0] — 2026-10-29
7
+
8
+ ### Ajouté
9
+ - **Le paiement ponctuel de la charge** : `ctx.paiement.ouvrir({ moyen, montant, description, reference, metadata,
10
+ merci, annule })` ouvre une session chez une banque prépayée active (celle qui a un vendeur, 0.6) pour un achat à
11
+ l'unité de la charge (une place de cours de Speaking) ; `ctx.paiement.moyens` dit les moyens et leur devise ;
12
+ `ctx.paiement` est `null` sans banque. La banque confirme à `/api/paiement/charge/<moyen>` (webhook authentifié par
13
+ sa signature, hors CSRF et hors session) ou renvoie l'acheteur à `/api/paiement/charge-retour/<moyen>` (banque
14
+ réinterrogée, comme SATIM) ; le wagon remet le constat à `charge.paiementRegle({ reference, statut, commande, montant,
15
+ devise, moyen, metadata })`. Les chemins de retour sont locaux. La ligne `Payment` reste dans la base du wagon.
16
+ Essai T-ENG-16.
17
+
18
+ ## [0.6.1] — 2026-10-28
19
+
20
+ ### Corrigé
21
+ - **/compte d'une période prépayée dit ce qui a été payé** : le montant et la monnaie de la commande réglée
22
+ (`Payment`, @mostajs/payment), et sa durée (« Classe — 8 000 DZD — 1 mois »), au lieu du prix Stripe en euros par
23
+ mois. Relevé par le fondateur après un paiement Chargily sur Speaking. Sans commande retrouvée, le prix prépayé du
24
+ palier dans la monnaie de la banque. Les paiements prépayés réglés figurent parmi « Vos factures ».
25
+ - Le dinar s'affiche sans décimales (« 8 000 DZD »), comme dans wagon-ui, aussi sur l'écran du choix du paiement.
26
+ - Essais : T-ENG-15 ; T-WAG-13 (suite de conformité) vérifie le montant prépayé dans /compte.
27
+
28
+ ### Dépendances
29
+ - `@mostajs/wagon-ui` ^0.11.1.
30
+
6
31
  ## [0.6.0] — 2026-10-28
7
32
 
8
33
  ### Ajouté
package/README.md CHANGED
@@ -50,6 +50,8 @@ await demarrer({ config, charge });
50
50
 
51
51
  **Le paiement est au choix de l'utilisateur**, parmi les moyens actifs. Un moyen est actif quand ses clés sont dans le `.env` et que son commutateur n'est pas sur `off`. Une banque prépayée doit en plus être proposée par la configuration.
52
52
 
53
+ **Un achat à l'unité de la charge (0.7).** `ctx.paiement.ouvrir({ moyen, montant, description, reference, metadata, merci, annule })` ouvre un paiement ponctuel (montant en unités mineures) chez une banque prépayée active ; `ctx.paiement.moyens` les liste, `ctx.paiement` est `null` sans banque. La banque confirme à `/api/paiement/charge/<moyen>` (signature) ou y renvoie l'acheteur (`/api/paiement/charge-retour/<moyen>`) ; le wagon appelle `charge.paiementRegle({ reference, statut, commande, montant, devise, moyen, metadata })`, que la charge écrit idempotent.
54
+
53
55
  ## Juger une greffe
54
56
 
55
57
  La suite de conformité est publiée avec le moteur. Elle juge la greffe du dossier courant, ou celle que désigne `WAGON_GREFFE` :
package/llms.txt CHANGED
@@ -45,10 +45,11 @@ encore immatriculée → `PAIEMENT_VENDEUR_DZD=…`, `PAIEMENT_VENDEUR_EUR=` (vi
45
45
  ## CONTRAT DE LA CHARGE (charge/index.mjs)
46
46
  liens · ressources (quota { cle, limite, periode } | plafond { cle, limite, plafond: true }) · accueil (fiche { fr: { titre, lede, actions, preuves } }, ou 0.3 : async (ctx) => ({ titre, contenu, disposition, application, console })) · dispositions (0.3 : { nom: (p) => corps }) · textes ·
47
47
  routes(req, res, ctx) (passent en premier ; true si servie) · conformite (essai de quota T-WAG-3) · amorcer() (base au
48
- démarrage) · exporterCompte(id), effacerCompte(id) (RGPD) · sitemap() · sauvegarde() · permissions (0.11 :
48
+ démarrage) · exporterCompte(id), effacerCompte(id) (RGPD) · sitemap() · sauvegarde() · paiementRegle(p) (0.7) · permissions (0.11 :
49
49
  { '/page': { microphone: 'self' }, '/dossier/*': … }, garde-http ≥ 0.2).
50
50
  ctx : chemin, methode, lang, t, ui, page({ titre, contenu }), envoyer, json, rediriger, formulaire, corpsJson({ strict }),
51
51
  compte, consommer(cle), refuser(v), palier({ compte? }), plafond(cle, n, { compte? }), courriel({ to, subject, text, html? }) → bool, admin.
52
+ 0.7 : ctx.paiement (null sans banque prépayée active) = { moyens: [{ moyen, devise }], ouvrir({ moyen, montant (unités MINEURES), description, reference, metadata, merci: '/chemin', annule: '/chemin' }) → { ok, url, commande } | { ok: false, raison } } — paiement PONCTUEL de la charge (une place de cours…). La banque confirme à /api/paiement/charge/<moyen> (webhook, signature) ou renvoie l'acheteur à /api/paiement/charge-retour/<moyen> (SATIM) ; le wagon appelle alors charge.paiementRegle({ reference, statut: paid|failed|refunded|…, commande, montant (mineures), devise, moyen, metadata }), à écrire IDEMPOTENT (une banque rejoue). merci/annule : chemins locaux seulement.
52
53
  0.4 : /inscription?suite=/chemin et /connexion?suite=/chemin — retour à une page LOCALE après inscription (si ni clé ni paiement à montrer ; sinon bouton « Continuer ») ou connexion par mot de passe ; //hôte et adresses absolues ignorés.
53
54
  0.3 : ctx.page({ disposition: 'vitrine'|'application'|'console'|'nu'|<fournie>, application: { retour, titre, avatar, onglets, langues }, console: { menu, contexte, utilisateur } }) (wagon-ui 0.10) ; config.jetons { rayon, police, policeTitres, ombre, fond } ; config.marque.logo / logoClair (SVG en ligne ou chemin /…).
54
55
  0.2 : { compte } = identifiant d'un compte DÉSIGNÉ (requête sans session bornée par le palier d'un compte rattaché par la charge).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/wagon-engine",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Le wagon d'un SaaS, une fois pour toutes : comptes, connexion, paliers, quotas, paiement (Stripe, Chargily, SATIM), espace client, administration, pages légales, fr/en/ar. Une greffe n'apporte que wagon.config.mjs, charge/, legal/ et son .env ; la suite de conformité publiée juge la greffe.",
5
5
  "keywords": [
6
6
  "mostajs",
@@ -52,7 +52,7 @@
52
52
  "@mostajs/orm": "^2.12.1",
53
53
  "@mostajs/payment": "^2.7.0",
54
54
  "@mostajs/subscriptions-plan": "^0.9.0",
55
- "@mostajs/wagon-ui": "^0.11.0",
55
+ "@mostajs/wagon-ui": "^0.11.1",
56
56
  "better-sqlite3": "^12.11.1",
57
57
  "stripe": "^17.7.0"
58
58
  },
@@ -590,6 +590,11 @@ const PRIX_DZD = PAYANT && (PAYANT.prepaid ?? {}).DZD?.month;
590
590
  const compte = (await req(base, '/compte', { cookie: c })).texte;
591
591
  expect(compte.includes(PAYANT.name)).toBe(true);
592
592
  expect(/payé jusqu’au/.test(compte)).toBe(true);
593
+ // 0.6.1 — le palier dit ce qui a été payé, dans la monnaie de la banque ; pas le prix Stripe en euros (défaut du 28/10/2026)
594
+ const [, devise, prixPrepaye] = OFFRE;
595
+ const sansEspace = (x) => x.replace(/[\s\u00a0\u202f]/g, '');
596
+ expect(sansEspace(compte).includes(sansEspace(String(Number(prixPrepaye.month) / 100)))).toBe(true);
597
+ expect([compte.includes(devise) || compte.includes('DA') || compte.includes('€'), Number(PAYANT.price) > 0 && devise !== 'EUR' ? compte.includes('€') : false]).toEqual([true, false]);
593
598
  expect((await webhook('ok')).status).toBe(200); // rejeu sans effet
594
599
  expect((await req(base, '/api/paiement/webhook/inconnue', { method: 'POST', json: '{}', headers: { 'content-type': 'application/json' } })).status).toBe(404);
595
600
  await C.fermer();
package/src/wagon.mjs CHANGED
@@ -25,7 +25,8 @@ import { creerWagonUi, esc } from '@mostajs/wagon-ui';
25
25
  import { creerComptes, schemasComptes } from '@mostajs/comptes';
26
26
  import { createBilling, createStripeSale, createPrepaidSale, billingSchemas, checkLimits, assertMonotonicPlans } from '@mostajs/subscriptions-plan/server';
27
27
  import { PaymentSchema } from '@mostajs/payment';
28
- import { venteAutorisee, vendeursFromEnv } from '@mostajs/payment/server';
28
+ import * as paymentServer from '@mostajs/payment/server';
29
+ const { venteAutorisee, vendeursFromEnv } = paymentServer;
29
30
  import { formatNumber, resolveFormatLocale } from '@mostajs/i18n';
30
31
  import { createPasswordAuth, createEmailCodeAuth, SessionSchema, ipClient, memoryLimiter } from '@mostajs/auth-lite';
31
32
  import { emailCodeEmailHtml, emailCodeEmailText, emailCodeForm, emailLinkConfirmPage, AUTH_CSS } from '@mostajs/auth-ui';
@@ -118,7 +119,9 @@ const DOMAINES_BANQUES = {
118
119
  paypal: 'https://www.paypal.com https://www.sandbox.paypal.com',
119
120
  satim: 'https://cib.satim.dz https://test.satim.dz',
120
121
  };
121
- const montant = (mineures, devise, lang) => formatNumber(resolveFormatLocale(lang), Number(mineures) / 100, { style: 'currency', currency: devise });
122
+ // 0.6.1 — le dinar sans décimales, comme on l'écrit en Algérie (et comme wagon-ui ≥ 0.10.1) : « 8 000 DZD ».
123
+ const montant = (mineures, devise, lang) => formatNumber(resolveFormatLocale(lang), Number(mineures) / 100,
124
+ { style: 'currency', currency: devise, ...(devise === 'DZD' ? { maximumFractionDigits: 0, minimumFractionDigits: 0 } : {}) });
122
125
 
123
126
  /** Le message d'un PLAFOND atteint (wagon-ui n'a que celui des clés d'API). */
124
127
  const PLAFOND_ATTEINT = { fr: 'Plafond de votre palier atteint : {n} ({ressource}).', en: 'Your tier limit is reached: {n} ({ressource}).', ar: 'بلغتَ حدّ فئتك: {n} ({ressource}).' };
@@ -257,6 +260,53 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
257
260
  return faits;
258
261
  }
259
262
  if (prepaye && Object.keys(banques).some((b) => SANS_WEBHOOK.includes(b))) setInterval(reprendrePaiements, 10 * 60 * 1000).unref?.();
263
+
264
+ /**
265
+ * 0.7 — le PAIEMENT PONCTUEL de la charge (une place de cours, un achat à l'unité), par les banques prépayées
266
+ * actives (celles qui ont un vendeur) : `ctx.paiement.ouvrir(…)` ouvre la session chez la banque ; la banque confirme
267
+ * à /api/paiement/charge/<moyen> (webhook) ou y renvoie l'acheteur (/api/paiement/charge-retour/<moyen>, SATIM). Le
268
+ * paiement se constate par @mostajs/payment (signature vérifiée, ou banque réinterrogée : rien de l'URL n'est cru),
269
+ * puis le wagon appelle `charge.paiementRegle({ reference, statut, commande, montant, devise, moyen, metadata })`.
270
+ * Le wagon ne garde que la ligne Payment ; ce que le paiement règle (une réservation…) est l'affaire de la charge,
271
+ * qui doit être idempotente (une banque rejoue ses webhooks).
272
+ */
273
+ const portPaiement = async () => paiement?.port?.createPaymentCheckout ? paiement.port : paymentServer;
274
+ const CHEMIN_LOCAL = /^\/(?!\/)[^\s]*$/;
275
+ const paiementCharge = Object.keys(banques).length ? Object.freeze({
276
+ moyens: Object.entries(banques).map(([moyen, devise]) => ({ moyen, devise: String(devise).toUpperCase() })),
277
+ /** `montant` en unités MINEURES ; `merci` et `annule` : des chemins de la greffe. Rend { ok, url, commande } ou { ok: false, raison }. */
278
+ async ouvrir({ moyen, montant, description = '', reference, metadata = {}, merci = '/', annule = '/' }) {
279
+ const devise = banques[moyen];
280
+ if (!devise) return { ok: false, raison: `moyen non proposé : ${moyen}` };
281
+ const m = Math.round(Number(montant));
282
+ if (!(m > 0)) return { ok: false, raison: 'montant invalide' };
283
+ if (!reference || !CHEMIN_LOCAL.test(merci) || !CHEMIN_LOCAL.test(annule)) return { ok: false, raison: 'référence ou chemins de retour invalides' };
284
+ const commande = `ch_${String(reference).replace(/[^\w-]/g, '').slice(0, 40)}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
285
+ try {
286
+ const r = await (await portPaiement()).createPaymentCheckout({
287
+ dialect: d, providerName: moyen, orderId: commande, amount: m / 100, currency: String(devise).toUpperCase(), description,
288
+ successUrl: `${origine}${merci}`, cancelUrl: `${origine}${annule}`,
289
+ webhookUrl: `${origine}/api/paiement/charge/${moyen}`, returnUrl: `${origine}/api/paiement/charge-retour/${moyen}`,
290
+ metadata: { ...metadata, wagon_charge: String(reference), wagon_merci: merci, wagon_annule: annule },
291
+ });
292
+ if (!r?.checkout?.url) return { ok: false, raison: 'la banque n’a pas rendu d’adresse de paiement' };
293
+ return { ok: true, url: r.checkout.url, commande };
294
+ } catch (e) { return { ok: false, raison: String(e?.message ?? e) }; }
295
+ },
296
+ }) : null;
297
+ /** Constate un paiement de la charge et le lui remet : { status, paye, meta }. */
298
+ async function reglerCharge(moyen, corpsBrut, headers) {
299
+ if (!banques[moyen]) return { status: 404, paye: false, meta: {} };
300
+ const r = await (await portPaiement()).settlePaymentFromWebhook({ dialect: d, providerName: moyen, body: corpsBrut, headers });
301
+ if (!r?.ok) return { status: 400, paye: false, meta: {} };
302
+ const pay = r.payment;
303
+ const meta = typeof pay?.metadata === 'string' ? JSON.parse(pay.metadata || '{}') : (pay?.metadata ?? {});
304
+ if (!pay || !r.status || !meta.wagon_charge) return { status: 200, paye: false, meta };
305
+ try {
306
+ await charge.paiementRegle?.({ reference: meta.wagon_charge, statut: r.status, commande: String(r.orderId ?? pay.orderId), montant: Math.round(Number(pay.amount) * 100), devise: pay.currency, moyen, metadata: meta });
307
+ } catch (e) { journal.warn(`wagon : paiementRegle de la charge : ${e?.message ?? e}`); return { status: 500, paye: false, meta }; }
308
+ return { status: 200, paye: r.status === 'paid', meta };
309
+ }
260
310
  const admins = new Set([...(config.admins ?? []), ...String(getEnv('WAGON_ADMINS', '') ?? '').split(',')].map((x) => x.trim().toLowerCase()).filter(Boolean));
261
311
  /** Un courriel est administrateur s'il est DÉCLARÉ et CONFIRMÉ. */
262
312
  const estAdministrateur = async (courriel) => {
@@ -380,7 +430,36 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
380
430
  return servis.get(lang);
381
431
  };
382
432
 
383
- async function vueEspace(compte) {
433
+ /**
434
+ * 0.6.1 — ce qui a été PAYÉ pour une période prépayée (Chargily, SATIM…) : le montant et la monnaie de la commande, et sa
435
+ * durée. /compte montrait le prix Stripe en euros par mois à une école qui avait payé 8 000 DZD (28/10/2026).
436
+ */
437
+ async function tarifPrepaye(sub, plan, lang) {
438
+ if (!sub?.provider || sub.stripeSubId || sub.provider === 'stripe') return null;
439
+ const T = TEXTES_PAIEMENT[lang] ?? TEXTES_PAIEMENT.fr;
440
+ const dire = (centimes, devise, periode) => `${montant(centimes, String(devise).toUpperCase(), lang)}${T[periode] ? ` — ${T[periode]}` : ''}`;
441
+ // La commande payée (@mostajs/payment la garde) ; sinon, le prix prépayé du palier dans la monnaie de la banque.
442
+ const p = sub.prepaidOrderId ? await d.findOne(S.Payment, { orderId: String(sub.prepaidOrderId) }).catch(() => null) : null;
443
+ if (p && Number(p.amount) > 0) {
444
+ const meta = typeof p.metadata === 'string' ? JSON.parse(p.metadata || '{}') : (p.metadata ?? {});
445
+ return dire(Math.round(Number(p.amount) * 100), p.currency, meta.wagon_periode);
446
+ }
447
+ const devise = banques[sub.provider];
448
+ const pp = typeof plan?.prepaid === 'string' ? JSON.parse(plan.prepaid || 'null') : plan?.prepaid;
449
+ const jours = (new Date(sub.currentPeriodEnd).getTime() - new Date(sub.currentPeriodStart).getTime()) / 86400000;
450
+ const periode = jours >= 300 ? 'year' : 'month';
451
+ const centimes = Number(pp?.[devise]?.[periode]);
452
+ return devise && centimes > 0 ? dire(centimes, devise, periode) : null;
453
+ }
454
+ /** 0.6.1 — les paiements prépayés réglés du compte, parmi les factures (montant en unités mineures, comme Stripe). */
455
+ async function paiementsPrepayes(compte) {
456
+ return (await d.find(S.Payment, { status: 'paid' }).catch(() => [])).filter((p) => {
457
+ const meta = typeof p.metadata === 'string' ? JSON.parse(p.metadata || '{}') : (p.metadata ?? {});
458
+ return String(meta[META.account] ?? '') === String(compte.id);
459
+ }).map((p) => ({ createdAt: p.createdAt, amount: Math.round(Number(p.amount) * 100), currency: p.currency, status: 'paid', hostedUrl: null }));
460
+ }
461
+
462
+ async function vueEspace(compte, lang = 'fr') {
384
463
  const actif = await facturation.activePlan(String(compte.id));
385
464
  const quotas = {};
386
465
  for (const r of periodiques) {
@@ -388,8 +467,10 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
388
467
  quotas[r.cle] = { courant: v.current, limite: v.limit === -1 ? null : v.limit, restant: v.limit === -1 ? null : Math.max(0, v.limit - v.current) };
389
468
  }
390
469
  const cles = (await comptes.cles.lister(String(compte.id))).map((c) => ({ prefix: c.prefix, label: c.label, enabled: c.enabled, usageCount: c.usageCount }));
391
- const factures = (await d.find(S.Invoice, { account: String(compte.id) })).map((f) => ({ createdAt: f.createdAt, amount: f.amount, currency: f.currency, status: f.status, hostedUrl: f.hostedUrl }));
392
- return { plan: actif?.plan ?? null, quotas, cles, factures, portailPossible: !!(stripe && compte.stripeCustomerId), limites: actif?.limits ?? {} };
470
+ const factures = [...(await d.find(S.Invoice, { account: String(compte.id) })).map((f) => ({ createdAt: f.createdAt, amount: f.amount, currency: f.currency, status: f.status, hostedUrl: f.hostedUrl })),
471
+ ...(await paiementsPrepayes(compte))].sort((a, b) => String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')));
472
+ const tarif = await tarifPrepaye(actif?.subscription, actif?.plan, lang);
473
+ return { plan: actif?.plan ?? null, ...(tarif ? { tarif } : {}), quotas, cles, factures, portailPossible: !!(stripe && compte.stripeCustomerId), limites: actif?.limits ?? {} };
393
474
  }
394
475
 
395
476
  /**
@@ -523,6 +604,19 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
523
604
  return rediriger(r.paid ? '/paiement/merci' : '/tarifs');
524
605
  }
525
606
 
607
+ // 0.7 — les paiements ponctuels de la charge : webhook (signature) et retour de l'acheteur (banque réinterrogée).
608
+ if (chemin.startsWith('/api/paiement/charge/') && methode === 'POST') {
609
+ if (!paiementCharge) return json(503, { ok: false, error: 'paiement non configuré' });
610
+ const r = await reglerCharge(decodeURIComponent(chemin.slice('/api/paiement/charge/'.length)), (await corps()).toString('utf8'), req.headers);
611
+ return json(r.status, { ok: r.status === 200 });
612
+ }
613
+ if (chemin.startsWith('/api/paiement/charge-retour/') && (methode === 'GET' || methode === 'POST')) {
614
+ if (!paiementCharge) return rediriger('/');
615
+ const r = await reglerCharge(decodeURIComponent(chemin.slice('/api/paiement/charge-retour/'.length)), methode === 'GET' ? url.searchParams.toString() : (await corps()).toString('utf8'), req.headers);
616
+ const vers = r.paye ? r.meta.wagon_merci : r.meta.wagon_annule;
617
+ return rediriger(typeof vers === 'string' && CHEMIN_LOCAL.test(vers) ? vers : '/');
618
+ }
619
+
526
620
  // La porte de service : une clé d'API ouvre /api/v1/* ; sans clé valable, 401 — jamais une page.
527
621
  let compteCle = null;
528
622
  if (chemin.startsWith('/api/v1/')) {
@@ -598,7 +692,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
598
692
  if (!transport) return false;
599
693
  try { await transport.envoyer(m); return true; } catch (e) { journal.warn(`wagon : courriel de la charge non parti : ${e?.message ?? e}`); return false; }
600
694
  };
601
- const ctx = { chemin, methode, lang, t, ui, page, envoyer, json, rediriger, formulaire, corpsJson, compte, consommer, refuser, palier, plafond, courriel, admin: estAdmin && !compteCle };
695
+ const ctx = { chemin, methode, lang, t, ui, page, envoyer, json, rediriger, formulaire, corpsJson, compte, consommer, refuser, palier, plafond, courriel, admin: estAdmin && !compteCle,
696
+ paiement: paiementCharge }; // 0.7 — null sans banque prépayée active
602
697
  if (charge.routes && (await charge.routes(req, res, ctx))) return true;
603
698
  if (chemin.startsWith('/api/')) return json(404, { ok: false, error: 'introuvable' });
604
699
 
@@ -754,7 +849,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
754
849
  const fin = sub?.currentPeriodEnd && !sub?.stripeSubId && Number(actif?.plan?.price) > 0
755
850
  ? `<p class="hint">${esc(T.jusquau.replace('{plan}', actif.plan.name).replace('{date}', new Date(sub.currentPeriodEnd).toLocaleDateString(resolveFormatLocale(lang), { day: 'numeric', month: 'long', year: 'numeric' })))}</p>` : '';
756
851
  const gestion = `<section class="gestion-compte"><h2>${esc(TC.gerer)}</h2><ul>${methodeConnexion === 'motdepasse' ? `<li><a href="/compte/mot-de-passe">${esc(TC.motDePasse)}</a></li>` : ''}<li><a href="/compte/export">${esc(TC.exporter)}</a></li><li><a href="/compte/supprimer">${esc(TC.supprimer)}</a></li></ul><form method="post" action="/deconnexion"><button class="bouton">${esc(TC.deconnexion)}</button></form></section>`;
757
- return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c) }) + fin + gestion }));
852
+ return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c, lang) }) + fin + gestion }));
758
853
  }
759
854
  // 0.5 — changer son mot de passe : l'actuel d'abord ; les AUTRES sessions sont fermées, celle-ci rouverte.
760
855
  if (chemin === '/compte/mot-de-passe' && methodeConnexion === 'motdepasse') {
@@ -773,7 +868,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
773
868
  // 0.5 — partir avec ses données (RGPD, portabilité) : un fichier JSON.
774
869
  if (chemin === '/compte/export' && methode === 'GET') {
775
870
  const u = await d.findOne(S.User, { email: String(qui.utilisateur.courriel) });
776
- const vue = await vueEspace(c);
871
+ const vue = await vueEspace(c, lang);
777
872
  const donnees = {
778
873
  exporteLe: new Date().toISOString(), service: config.marque.nom,
779
874
  utilisateur: { courriel: u?.email, prenom: u?.firstName, nom: u?.lastName, langue: u?.locale, creeLe: u?.createdAt },
@@ -813,8 +908,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
813
908
  const f = await formulaire();
814
909
  try {
815
910
  const k = await comptes.cles.creer(String(c.id), { label: String(f.libelle ?? '').slice(0, 60), portees: {} });
816
- return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c), cleNeuve: k.full }) }));
817
- } catch (e) { return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c), erreur: e.code === 'PLAFOND_CLES' ? t('espace.plafondAtteint', { n: (await vueEspace(c)).limites.maxCles }) : e.message }) })); }
911
+ return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c, lang), cleNeuve: k.full }) }));
912
+ } catch (e) { return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c, lang), erreur: e.code === 'PLAFOND_CLES' ? t('espace.plafondAtteint', { n: (await vueEspace(c, lang)).limites.maxCles }) : e.message }) })); }
818
913
  }
819
914
  if (chemin === '/compte/cles/revoquer' && methode === 'POST') {
820
915
  await comptes.cles.revoquer(String(c.id), String((await formulaire()).prefix ?? ''));