@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 +25 -0
- package/README.md +2 -0
- package/llms.txt +2 -1
- package/package.json +2 -2
- package/src/conformite/conformite.test.mjs +5 -0
- package/src/wagon.mjs +105 -10
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? ''));
|