@mostajs/wagon-engine 0.6.1 → 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 +12 -0
- package/README.md +2 -0
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/src/wagon.mjs +64 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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
|
+
|
|
6
18
|
## [0.6.1] — 2026-10-28
|
|
7
19
|
|
|
8
20
|
### Corrigé
|
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",
|
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';
|
|
@@ -259,6 +260,53 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
259
260
|
return faits;
|
|
260
261
|
}
|
|
261
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
|
+
}
|
|
262
310
|
const admins = new Set([...(config.admins ?? []), ...String(getEnv('WAGON_ADMINS', '') ?? '').split(',')].map((x) => x.trim().toLowerCase()).filter(Boolean));
|
|
263
311
|
/** Un courriel est administrateur s'il est DÉCLARÉ et CONFIRMÉ. */
|
|
264
312
|
const estAdministrateur = async (courriel) => {
|
|
@@ -556,6 +604,19 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
556
604
|
return rediriger(r.paid ? '/paiement/merci' : '/tarifs');
|
|
557
605
|
}
|
|
558
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
|
+
|
|
559
620
|
// La porte de service : une clé d'API ouvre /api/v1/* ; sans clé valable, 401 — jamais une page.
|
|
560
621
|
let compteCle = null;
|
|
561
622
|
if (chemin.startsWith('/api/v1/')) {
|
|
@@ -631,7 +692,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
631
692
|
if (!transport) return false;
|
|
632
693
|
try { await transport.envoyer(m); return true; } catch (e) { journal.warn(`wagon : courriel de la charge non parti : ${e?.message ?? e}`); return false; }
|
|
633
694
|
};
|
|
634
|
-
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
|
|
635
697
|
if (charge.routes && (await charge.routes(req, res, ctx))) return true;
|
|
636
698
|
if (chemin.startsWith('/api/')) return json(404, { ok: false, error: 'introuvable' });
|
|
637
699
|
|