@mostajs/wagon-engine 0.6.0 → 0.6.1
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 +13 -0
- package/package.json +2 -2
- package/src/conformite/conformite.test.mjs +5 -0
- package/src/wagon.mjs +41 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
Format : Keep a Changelog ; versions : SemVer.
|
|
5
5
|
|
|
6
|
+
## [0.6.1] — 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Corrigé
|
|
9
|
+
- **/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
|
|
10
|
+
(`Payment`, @mostajs/payment), et sa durée (« Classe — 8 000 DZD — 1 mois »), au lieu du prix Stripe en euros par
|
|
11
|
+
mois. Relevé par le fondateur après un paiement Chargily sur Speaking. Sans commande retrouvée, le prix prépayé du
|
|
12
|
+
palier dans la monnaie de la banque. Les paiements prépayés réglés figurent parmi « Vos factures ».
|
|
13
|
+
- Le dinar s'affiche sans décimales (« 8 000 DZD »), comme dans wagon-ui, aussi sur l'écran du choix du paiement.
|
|
14
|
+
- Essais : T-ENG-15 ; T-WAG-13 (suite de conformité) vérifie le montant prépayé dans /compte.
|
|
15
|
+
|
|
16
|
+
### Dépendances
|
|
17
|
+
- `@mostajs/wagon-ui` ^0.11.1.
|
|
18
|
+
|
|
6
19
|
## [0.6.0] — 2026-10-28
|
|
7
20
|
|
|
8
21
|
### Ajouté
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/wagon-engine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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
|
@@ -118,7 +118,9 @@ const DOMAINES_BANQUES = {
|
|
|
118
118
|
paypal: 'https://www.paypal.com https://www.sandbox.paypal.com',
|
|
119
119
|
satim: 'https://cib.satim.dz https://test.satim.dz',
|
|
120
120
|
};
|
|
121
|
-
|
|
121
|
+
// 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 ».
|
|
122
|
+
const montant = (mineures, devise, lang) => formatNumber(resolveFormatLocale(lang), Number(mineures) / 100,
|
|
123
|
+
{ style: 'currency', currency: devise, ...(devise === 'DZD' ? { maximumFractionDigits: 0, minimumFractionDigits: 0 } : {}) });
|
|
122
124
|
|
|
123
125
|
/** Le message d'un PLAFOND atteint (wagon-ui n'a que celui des clés d'API). */
|
|
124
126
|
const PLAFOND_ATTEINT = { fr: 'Plafond de votre palier atteint : {n} ({ressource}).', en: 'Your tier limit is reached: {n} ({ressource}).', ar: 'بلغتَ حدّ فئتك: {n} ({ressource}).' };
|
|
@@ -380,7 +382,36 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
380
382
|
return servis.get(lang);
|
|
381
383
|
};
|
|
382
384
|
|
|
383
|
-
|
|
385
|
+
/**
|
|
386
|
+
* 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
|
|
387
|
+
* durée. /compte montrait le prix Stripe en euros par mois à une école qui avait payé 8 000 DZD (28/10/2026).
|
|
388
|
+
*/
|
|
389
|
+
async function tarifPrepaye(sub, plan, lang) {
|
|
390
|
+
if (!sub?.provider || sub.stripeSubId || sub.provider === 'stripe') return null;
|
|
391
|
+
const T = TEXTES_PAIEMENT[lang] ?? TEXTES_PAIEMENT.fr;
|
|
392
|
+
const dire = (centimes, devise, periode) => `${montant(centimes, String(devise).toUpperCase(), lang)}${T[periode] ? ` — ${T[periode]}` : ''}`;
|
|
393
|
+
// La commande payée (@mostajs/payment la garde) ; sinon, le prix prépayé du palier dans la monnaie de la banque.
|
|
394
|
+
const p = sub.prepaidOrderId ? await d.findOne(S.Payment, { orderId: String(sub.prepaidOrderId) }).catch(() => null) : null;
|
|
395
|
+
if (p && Number(p.amount) > 0) {
|
|
396
|
+
const meta = typeof p.metadata === 'string' ? JSON.parse(p.metadata || '{}') : (p.metadata ?? {});
|
|
397
|
+
return dire(Math.round(Number(p.amount) * 100), p.currency, meta.wagon_periode);
|
|
398
|
+
}
|
|
399
|
+
const devise = banques[sub.provider];
|
|
400
|
+
const pp = typeof plan?.prepaid === 'string' ? JSON.parse(plan.prepaid || 'null') : plan?.prepaid;
|
|
401
|
+
const jours = (new Date(sub.currentPeriodEnd).getTime() - new Date(sub.currentPeriodStart).getTime()) / 86400000;
|
|
402
|
+
const periode = jours >= 300 ? 'year' : 'month';
|
|
403
|
+
const centimes = Number(pp?.[devise]?.[periode]);
|
|
404
|
+
return devise && centimes > 0 ? dire(centimes, devise, periode) : null;
|
|
405
|
+
}
|
|
406
|
+
/** 0.6.1 — les paiements prépayés réglés du compte, parmi les factures (montant en unités mineures, comme Stripe). */
|
|
407
|
+
async function paiementsPrepayes(compte) {
|
|
408
|
+
return (await d.find(S.Payment, { status: 'paid' }).catch(() => [])).filter((p) => {
|
|
409
|
+
const meta = typeof p.metadata === 'string' ? JSON.parse(p.metadata || '{}') : (p.metadata ?? {});
|
|
410
|
+
return String(meta[META.account] ?? '') === String(compte.id);
|
|
411
|
+
}).map((p) => ({ createdAt: p.createdAt, amount: Math.round(Number(p.amount) * 100), currency: p.currency, status: 'paid', hostedUrl: null }));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function vueEspace(compte, lang = 'fr') {
|
|
384
415
|
const actif = await facturation.activePlan(String(compte.id));
|
|
385
416
|
const quotas = {};
|
|
386
417
|
for (const r of periodiques) {
|
|
@@ -388,8 +419,10 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
388
419
|
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
420
|
}
|
|
390
421
|
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
|
-
|
|
422
|
+
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 })),
|
|
423
|
+
...(await paiementsPrepayes(compte))].sort((a, b) => String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')));
|
|
424
|
+
const tarif = await tarifPrepaye(actif?.subscription, actif?.plan, lang);
|
|
425
|
+
return { plan: actif?.plan ?? null, ...(tarif ? { tarif } : {}), quotas, cles, factures, portailPossible: !!(stripe && compte.stripeCustomerId), limites: actif?.limits ?? {} };
|
|
393
426
|
}
|
|
394
427
|
|
|
395
428
|
/**
|
|
@@ -754,7 +787,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
754
787
|
const fin = sub?.currentPeriodEnd && !sub?.stripeSubId && Number(actif?.plan?.price) > 0
|
|
755
788
|
? `<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
789
|
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 }));
|
|
790
|
+
return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c, lang) }) + fin + gestion }));
|
|
758
791
|
}
|
|
759
792
|
// 0.5 — changer son mot de passe : l'actuel d'abord ; les AUTRES sessions sont fermées, celle-ci rouverte.
|
|
760
793
|
if (chemin === '/compte/mot-de-passe' && methodeConnexion === 'motdepasse') {
|
|
@@ -773,7 +806,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
773
806
|
// 0.5 — partir avec ses données (RGPD, portabilité) : un fichier JSON.
|
|
774
807
|
if (chemin === '/compte/export' && methode === 'GET') {
|
|
775
808
|
const u = await d.findOne(S.User, { email: String(qui.utilisateur.courriel) });
|
|
776
|
-
const vue = await vueEspace(c);
|
|
809
|
+
const vue = await vueEspace(c, lang);
|
|
777
810
|
const donnees = {
|
|
778
811
|
exporteLe: new Date().toISOString(), service: config.marque.nom,
|
|
779
812
|
utilisateur: { courriel: u?.email, prenom: u?.firstName, nom: u?.lastName, langue: u?.locale, creeLe: u?.createdAt },
|
|
@@ -813,8 +846,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
813
846
|
const f = await formulaire();
|
|
814
847
|
try {
|
|
815
848
|
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 }) })); }
|
|
849
|
+
return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c, lang), cleNeuve: k.full }) }));
|
|
850
|
+
} 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
851
|
}
|
|
819
852
|
if (chemin === '/compte/cles/revoquer' && methode === 'POST') {
|
|
820
853
|
await comptes.cles.revoquer(String(c.id), String((await formulaire()).prefix ?? ''));
|