@mostajs/wagon-engine 0.5.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 +29 -0
- package/README.md +6 -0
- package/llms.txt +25 -4
- package/package.json +3 -3
- package/src/conformite/conformite.test.mjs +5 -0
- package/src/demarrer.mjs +4 -2
- package/src/wagon.mjs +72 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
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
|
+
|
|
19
|
+
## [0.6.0] — 2026-10-28
|
|
20
|
+
|
|
21
|
+
### Ajouté
|
|
22
|
+
- **Le vendeur de chaque devise** (`@mostajs/payment` ≥ 2.7) : `PAIEMENT_VENDEUR_<DEVISE>` dans le `.env`. Dès qu'une
|
|
23
|
+
telle variable est déclarée, une devise sans vendeur nommé n'est pas vendue : Stripe et les banques prépayées de
|
|
24
|
+
cette devise sont écartés, même avec leurs clés (un avertissement le dit au démarrage). `{{VENDEUR}}` des textes
|
|
25
|
+
légaux nomme le vendeur de chaque devise vendue, une ligne par devise ; `config.legal.VENDEUR` l'emporte s'il est
|
|
26
|
+
posé. `PAIEMENT_STRIPE_DEVISES` dit la devise de Stripe (défaut EUR), reprise par l'affichage des prix. Sans aucune
|
|
27
|
+
`PAIEMENT_VENDEUR_*`, rien ne change. Essai T-ENG-14.
|
|
28
|
+
- `creerWagon(…).moyens` : les moyens réellement proposés ; `demarrer` le rend tel quel.
|
|
29
|
+
- `llms.txt` et le README s'ouvrent sur la configuration du `.env` par `@mostajs/config` (chargement, cascade
|
|
30
|
+
`MOSTA_ENV`, valeur vide, priorité de l'environnement).
|
|
31
|
+
|
|
32
|
+
### Dépendances
|
|
33
|
+
- `@mostajs/payment` ^2.7.0.
|
|
34
|
+
|
|
6
35
|
## [0.5.0] — 2026-10-28
|
|
7
36
|
|
|
8
37
|
### Ajouté
|
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ await demarrer({ config, charge });
|
|
|
41
41
|
| `CHARGILY_SECRET_KEY` · `SATIM_MERCHANT_ID`, `SATIM_PASSWORD`, `SATIM_TERMINAL_ID` · `PAYPAL_…` | les banques prépayées proposées par `config.paiement.prepaye` |
|
|
42
42
|
| `PAIEMENT_STRIPE=off`, `PAIEMENT_CHARGILY=off`, `PAIEMENT_SATIM=off`, `PAIEMENT_PAYPAL=off` | **coupe un moyen** sans toucher à ses clés |
|
|
43
43
|
| `WAGON_ADMINS` | les courriels administrateurs (confirmés par un code reçu) |
|
|
44
|
+
| `PAIEMENT_VENDEUR_DZD`, `PAIEMENT_VENDEUR_EUR`… | 0.6 — **le vendeur de chaque devise** (dénomination, forme, adresse, immatriculations) ; dès qu'une est déclarée, une devise sans vendeur n'est pas vendue, et `{{VENDEUR}}` des CGV les nomme |
|
|
45
|
+
| `PAIEMENT_STRIPE_DEVISES` | 0.6 — la devise de Stripe (défaut `EUR`) |
|
|
46
|
+
|
|
47
|
+
**Le `.env` d'abord.** Toutes les clés passent par `getEnv` de `@mostajs/config` : cascade `MOSTA_ENV` (`PROD_CLÉ` l'emporte sur `CLÉ`), valeur vide = absente, et `loadEnv` n'écrase jamais une variable déjà posée par l'orchestrateur. Si `wagon.config.mjs` lit des variables, il appelle `loadEnv` lui-même, en tête.
|
|
48
|
+
|
|
49
|
+
**Un vendeur par devise (0.6).** Un SaaS qui vend en dinars par un auto-entrepreneur et en euros par une société pose `PAIEMENT_VENDEUR_DZD=…` et `PAIEMENT_VENDEUR_EUR=…`. Une devise déclarée vide (`PAIEMENT_VENDEUR_EUR=`, la société n'est pas encore immatriculée) n'est pas vendue : Stripe est écarté même avec sa clé. Sans aucune `PAIEMENT_VENDEUR_*`, rien ne change.
|
|
44
50
|
|
|
45
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.
|
|
46
52
|
|
package/llms.txt
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
> PayPal), espace client, administration, pages légales, robots/sitemap, sauvegarde, fr/en/ar. Une GREFFE n'apporte que
|
|
5
5
|
> wagon.config.mjs, charge/, legal/ et son .env. N2 (pack). Extrait du gabarit mosta-wagon 0.11.0 le 26/10/2026.
|
|
6
6
|
|
|
7
|
-
- Version: 0.
|
|
7
|
+
- Version: 0.6.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
8
8
|
- Chemin: mostajs/mosta-platform-stack/mosta-wagon-engine · gabarit d'une greffe : Entreprise/mosta-wagon
|
|
9
9
|
|
|
10
|
+
## ⚙️ D'EMBLÉE : LE .env ET LA CASCADE @mostajs/config
|
|
11
|
+
Une greffe se configure par SON `.env` (dossier courant de `demarrer`, ou `racine`). Avant tout :
|
|
12
|
+
1. **Qui le charge** : `demarrer` appelle `loadEnv({ path: <racine>/.env })` de `@mostajs/config`. Si `wagon.config.mjs`
|
|
13
|
+
lit lui-même des variables (getEnv au chargement du module), il doit appeler `loadEnv` EN TÊTE, avant de les lire :
|
|
14
|
+
`import { loadEnv, getEnv } from '@mostajs/config'; loadEnv({ path: join(process.cwd(), '.env') });`
|
|
15
|
+
Le second appel (celui de demarrer) n'écrase rien.
|
|
16
|
+
2. **loadEnv ne SURCHARGE pas** une variable déjà dans l'environnement : la valeur de l'orchestrateur (pm2, systemd,
|
|
17
|
+
conteneur) prime sur le fichier.
|
|
18
|
+
3. **Cascade** (première valeur NON VIDE) : `${MOSTA_ENV}_${CLÉ}` → `${CLÉ}` → défaut. Un seul .env, des profils
|
|
19
|
+
`TEST_`/`DEV_`/`PROD_`. Toutes les clés ci-dessous, et celles de `@mostajs/payment`, passent par `getEnv`.
|
|
20
|
+
4. **Vide = absent** : `WAGON_STRIPE_SECRET_KEY=` ne configure rien. Exception voulue : `PAIEMENT_VENDEUR_<DEVISE>=`
|
|
21
|
+
vide DÉCLARE la devise sans la nommer (elle n'est pas vendue).
|
|
22
|
+
5. Les secrets et les informations de l'entreprise sont posés par l'exploitant sur le serveur, jamais commités ;
|
|
23
|
+
`.env.example` de la greffe liste chaque clé, sans valeur.
|
|
24
|
+
|
|
10
25
|
## EXPORTS
|
|
11
|
-
- demarrer({ config, charge, racine = cwd, journal }) → { wagon, serveur, moyens: string[], fermer() }
|
|
26
|
+
- demarrer({ config, charge, racine = cwd, journal }) → { wagon, serveur, moyens: string[], fermer() } (moyens = wagon.moyens, 0.6)
|
|
12
27
|
- creerWagon({ config, charge, sgbd: string|{dialect,uri}, baseUrl, stripe?, webhookSecret?, transport?, journal?,
|
|
13
|
-
paiement?: { banques: { chargily:'DZD', … }, port? }, racine? }) → { handler, catalogue(), catalogueStripe(), fermer(), … }
|
|
28
|
+
paiement?: { banques: { chargily:'DZD', … }, port?, deviseStripe? }, racine? }) → { moyens, handler, catalogue(), catalogueStripe(), fermer(), … }
|
|
14
29
|
- schemasWagon() (sauvegarde) · sauvegarder({ cibles, driver, seau, recipients, garder }) · suivrePalette(…)
|
|
15
30
|
- '@mostajs/wagon-engine/conformite' = src/conformite/conformite.test.mjs (suite mjs-unit, juge WAGON_GREFFE ou cwd)
|
|
16
31
|
|
|
@@ -19,7 +34,13 @@ PORT, HOST · WAGON_BASE_URL · WAGON_DB_DIALECT (sqlite|postgres…), WAGON_SGB
|
|
|
19
34
|
WAGON_EMAIL_FROM (ou WAGON_COURRIEL_FICHIER pour les essais) · WAGON_STRIPE_SECRET_KEY|RESTRICTED_KEY, WAGON_STRIPE_WEBHOOK_SECRET ·
|
|
20
35
|
CHARGILY_SECRET_KEY|API_KEY · SATIM_MERCHANT_ID, SATIM_PASSWORD, SATIM_TERMINAL_ID · PAYPAL_CLIENT_ID, PAYPAL_SECRET,
|
|
21
36
|
PAYPAL_WEBHOOK_ID · PAIEMENT_<STRIPE|CHARGILY|SATIM|PAYPAL>=off (coupe le moyen) · WAGON_ADMINS · WAGON_INDEXER.
|
|
22
|
-
|
|
37
|
+
0.6 (@mostajs/payment ≥ 2.7) : PAIEMENT_VENDEUR_<DEVISE> (le vendeur de cette devise : dénomination, forme, adresse,
|
|
38
|
+
immatriculations) · PAIEMENT_STRIPE_DEVISES (la devise de Stripe, défaut EUR).
|
|
39
|
+
Moyen ACTIF = clés présentes ET commutateur ≠ off (ET, pour une banque prépayée, proposée par config.paiement.prepaye)
|
|
40
|
+
ET, 0.6, dès qu'une PAIEMENT_VENDEUR_* est déclarée, un vendeur NOMMÉ pour sa devise. `{{VENDEUR}}` des textes légaux
|
|
41
|
+
nomme alors le vendeur de chaque devise vendue (une ligne par devise, nom de la devise dans la langue de la page) ;
|
|
42
|
+
`config.legal.VENDEUR` l'emporte s'il est posé. Exemple : dinars par un auto-entrepreneur, euros par une société pas
|
|
43
|
+
encore immatriculée → `PAIEMENT_VENDEUR_DZD=…`, `PAIEMENT_VENDEUR_EUR=` (vide) : seule la vente en dinars s'ouvre.
|
|
23
44
|
|
|
24
45
|
## CONTRAT DE LA CHARGE (charge/index.mjs)
|
|
25
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 ·
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/wagon-engine",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"@mostajs/mailer": "^0.2.0",
|
|
51
51
|
"@mostajs/markdown-html": "^0.3.0",
|
|
52
52
|
"@mostajs/orm": "^2.12.1",
|
|
53
|
-
"@mostajs/payment": "^2.
|
|
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/demarrer.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { createServer } from 'node:http';
|
|
|
11
11
|
import { appendFileSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { loadEnv, getEnv } from '@mostajs/config';
|
|
14
|
+
import { devisesDuMoyen } from '@mostajs/payment/server';
|
|
14
15
|
import { creerWagon } from './wagon.mjs';
|
|
15
16
|
|
|
16
17
|
const coupe = (moyen) => getEnv(`PAIEMENT_${moyen.toUpperCase()}`, '') === 'off';
|
|
@@ -53,10 +54,11 @@ export async function demarrer({ config, charge, racine = process.cwd(), journal
|
|
|
53
54
|
stripe: cle ? new (await import('stripe')).default(cle) : null,
|
|
54
55
|
webhookSecret: getEnv('WAGON_STRIPE_WEBHOOK_SECRET', ''),
|
|
55
56
|
transport: await transport(),
|
|
56
|
-
|
|
57
|
+
// 0.6 — la devise de Stripe (PAIEMENT_STRIPE_DEVISES, défaut EUR) : creerWagon écarte une devise sans vendeur.
|
|
58
|
+
paiement: { banques, deviseStripe: devisesDuMoyen('stripe', ['EUR'])[0] },
|
|
57
59
|
});
|
|
58
60
|
await wagon.catalogue();
|
|
59
|
-
const moyens =
|
|
61
|
+
const moyens = wagon.moyens;
|
|
60
62
|
const serveur = createServer(wagon.handler);
|
|
61
63
|
const port = Number(getEnv('PORT', '4800'));
|
|
62
64
|
await new Promise((ok) => serveur.listen(port, getEnv('HOST', '127.0.0.1'), ok));
|
package/src/wagon.mjs
CHANGED
|
@@ -25,6 +25,7 @@ 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
29
|
import { formatNumber, resolveFormatLocale } from '@mostajs/i18n';
|
|
29
30
|
import { createPasswordAuth, createEmailCodeAuth, SessionSchema, ipClient, memoryLimiter } from '@mostajs/auth-lite';
|
|
30
31
|
import { emailCodeEmailHtml, emailCodeEmailText, emailCodeForm, emailLinkConfirmPage, AUTH_CSS } from '@mostajs/auth-ui';
|
|
@@ -117,7 +118,9 @@ const DOMAINES_BANQUES = {
|
|
|
117
118
|
paypal: 'https://www.paypal.com https://www.sandbox.paypal.com',
|
|
118
119
|
satim: 'https://cib.satim.dz https://test.satim.dz',
|
|
119
120
|
};
|
|
120
|
-
|
|
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 } : {}) });
|
|
121
124
|
|
|
122
125
|
/** Le message d'un PLAFOND atteint (wagon-ui n'a que celui des clés d'API). */
|
|
123
126
|
const PLAFOND_ATTEINT = { fr: 'Plafond de votre palier atteint : {n} ({ressource}).', en: 'Your tier limit is reached: {n} ({ressource}).', ar: 'بلغتَ حدّ فئتك: {n} ({ressource}).' };
|
|
@@ -165,12 +168,18 @@ const cookieDe = (req, nom) => (req.headers.cookie || '').split(';').map((s) =>
|
|
|
165
168
|
* @param {{ envoyer: Function } | null} [o.transport] les courriels (code de connexion)
|
|
166
169
|
* @param {{ banques?: Record<string, string>, port?: object }} [o.paiement] 0.3 — vente prépayée : les banques ACTIVES
|
|
167
170
|
* et leur devise (`{ chargily: 'DZD', paypal: 'EUR' }`, server.mjs ne garde que celles dont le .env porte les
|
|
168
|
-
* clés) ; `port` remplace @mostajs/payment (essais)
|
|
171
|
+
* clés) ; `port` remplace @mostajs/payment (essais) ; 0.6 — `deviseStripe` (défaut 'EUR') : la devise de Stripe
|
|
172
|
+
* 0.6 — une devise sans VENDEUR (`PAIEMENT_VENDEUR_<DEVISE>`, @mostajs/payment ≥ 2.7, règle en vigueur dès qu'une telle
|
|
173
|
+
* variable est déclarée) n'est pas vendue : Stripe et les banques de cette devise sont écartés ici, et `{{VENDEUR}}`
|
|
174
|
+
* des textes légaux nomme le vendeur de chaque devise vendue.
|
|
169
175
|
*/
|
|
170
176
|
/** 0.5 — le commit en service (posé par le déployeur dans le dossier de l'instance) et l'heure de démarrage. */
|
|
171
177
|
const lireRevision = (racine) => { try { return readFileSync(join(racine, 'REVISION'), 'utf8').trim() || null; } catch { return null; } };
|
|
172
178
|
export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null, webhookSecret = '', transport = null, journal = console, paiement = null, racine = process.cwd() }) {
|
|
173
179
|
const origine = String(baseUrl || '').replace(/\/$/, '');
|
|
180
|
+
// 0.6 — pas de vente dans une devise sans vendeur (même si les clés de sa banque sont posées).
|
|
181
|
+
const deviseStripe = String(paiement?.deviseStripe ?? 'EUR').toUpperCase();
|
|
182
|
+
if (stripe && !venteAutorisee(deviseStripe)) { journal.warn(`wagon : Stripe écarté — aucun vendeur pour ${deviseStripe} (PAIEMENT_VENDEUR_${deviseStripe})`); stripe = null; }
|
|
174
183
|
const REVISION = lireRevision(racine); const DEMARRE_LE = new Date().toISOString();
|
|
175
184
|
const https = /^https:/.test(origine);
|
|
176
185
|
const S = Object.fromEntries(schemasWagon().map((s) => [s.name, s]));
|
|
@@ -226,7 +235,11 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
226
235
|
accountOf: async (c) => (await d.findOne(S.Account, { stripeCustomerId: c }))?.id ?? null,
|
|
227
236
|
},
|
|
228
237
|
}) : null;
|
|
229
|
-
const banques =
|
|
238
|
+
const banques = Object.fromEntries(Object.entries(paiement?.banques ?? {}).filter(([b, dv]) => {
|
|
239
|
+
if (venteAutorisee(String(dv))) return true;
|
|
240
|
+
journal.warn(`wagon : ${b} écarté — aucun vendeur pour ${String(dv).toUpperCase()} (PAIEMENT_VENDEUR_${String(dv).toUpperCase()})`);
|
|
241
|
+
return false;
|
|
242
|
+
}));
|
|
230
243
|
const prepaye = Object.keys(banques).length ? createPrepaidSale({
|
|
231
244
|
billing: facturation, providers: banques, payment: paiement?.port, metadataKeys: { account: META.account, plan: META.plan, period: 'wagon_periode' },
|
|
232
245
|
// 0.10 — `return` : SATIM ne notifie pas le serveur, il renvoie l'acheteur (@mostajs/payment ≥ 2.6). Les banques à
|
|
@@ -329,12 +342,26 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
329
342
|
*/
|
|
330
343
|
const NOMS_MOYENS = { stripe: 'Stripe (carte bancaire)', chargily: 'Chargily (CIB, Edahabia)', paypal: 'PayPal', satim: 'SATIM (CIB)' };
|
|
331
344
|
const signales = new Set();
|
|
345
|
+
/**
|
|
346
|
+
* 0.6 — `{{VENDEUR}}` : le vendeur de chaque devise VENDUE (Stripe et les banques actives), d'après
|
|
347
|
+
* `PAIEMENT_VENDEUR_<DEVISE>` ; une ligne par devise. Vide si aucune n'a de vendeur : le texte n'est alors pas servi.
|
|
348
|
+
*/
|
|
349
|
+
function vendeurLegal(lang) {
|
|
350
|
+
const vendeurs = vendeursFromEnv();
|
|
351
|
+
const parDevise = new Map(stripe ? [[deviseStripe, ['stripe']]] : []);
|
|
352
|
+
for (const [b, dv] of Object.entries(banques)) { const k = String(dv).toUpperCase(); parDevise.set(k, [...(parDevise.get(k) ?? []), b]); }
|
|
353
|
+
let noms = null; try { noms = new Intl.DisplayNames([lang, 'fr'], { type: 'currency' }); } catch { /* langue inconnue */ }
|
|
354
|
+
return [...parDevise].filter(([dv]) => vendeurs[dv]).map(([dv, ms]) => {
|
|
355
|
+
const n = noms?.of(dv) ?? dv;
|
|
356
|
+
return `- **${n.charAt(0).toUpperCase()}${n.slice(1)} (${dv})** — ${ms.map((m) => NOMS_MOYENS[m] ?? m).join(', ')} : ${vendeurs[dv]}`;
|
|
357
|
+
}).join('\n');
|
|
358
|
+
}
|
|
332
359
|
function documentLegal(doc, lang) {
|
|
333
360
|
const dossier = config.dossierLegal ?? join(racine, 'legal'); // la greffe, plus le moteur
|
|
334
361
|
const f = [lang, 'fr'].map((l) => join(dossier, `${doc}.${l}.md`)).find((x) => existsSync(x));
|
|
335
362
|
if (!f) return { html: '', complet: false };
|
|
336
363
|
const moyens = [...(stripe ? ['stripe'] : []), ...Object.keys(paiement?.banques ?? {})].map((m) => NOMS_MOYENS[m] ?? m).join(', ');
|
|
337
|
-
const valeurs = { SERVICE: config.marque.nom, DOMAINE: origine.replace(/^https?:\/\//, ''), CONTACT_EMAIL: config.marque.courriel ?? '', PAIEMENT: moyens, ...(config.legal ?? {}) };
|
|
364
|
+
const valeurs = { SERVICE: config.marque.nom, DOMAINE: origine.replace(/^https?:\/\//, ''), CONTACT_EMAIL: config.marque.courriel ?? '', PAIEMENT: moyens, VENDEUR: vendeurLegal(lang), ...(config.legal ?? {}) };
|
|
338
365
|
const manquants = new Set();
|
|
339
366
|
const texte = readFileSync(f, 'utf8').replace(/\{\{(\w+)\}\}/g, (m, k) => { if (valeurs[k]) return String(valeurs[k]); manquants.add(k); return ''; });
|
|
340
367
|
const aTrancher = /⚖️|À FAIRE TRANCHER/.test(texte);
|
|
@@ -355,7 +382,36 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
355
382
|
return servis.get(lang);
|
|
356
383
|
};
|
|
357
384
|
|
|
358
|
-
|
|
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') {
|
|
359
415
|
const actif = await facturation.activePlan(String(compte.id));
|
|
360
416
|
const quotas = {};
|
|
361
417
|
for (const r of periodiques) {
|
|
@@ -363,8 +419,10 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
363
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) };
|
|
364
420
|
}
|
|
365
421
|
const cles = (await comptes.cles.lister(String(compte.id))).map((c) => ({ prefix: c.prefix, label: c.label, enabled: c.enabled, usageCount: c.usageCount }));
|
|
366
|
-
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 }))
|
|
367
|
-
|
|
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 ?? {} };
|
|
368
426
|
}
|
|
369
427
|
|
|
370
428
|
/**
|
|
@@ -618,7 +676,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
618
676
|
if (!p) return null;
|
|
619
677
|
const pp = typeof p.prepaid === 'string' ? JSON.parse(p.prepaid || 'null') : p.prepaid;
|
|
620
678
|
const l = [...new Set(Object.values(banques))].filter((dv) => Number(pp?.[dv]?.month) > 0).map((dv) => ({ centimes: Number(pp[dv].month), devise: dv }));
|
|
621
|
-
if (stripe && Number(p.price) > 0) l.push({ centimes: Number(p.price), devise:
|
|
679
|
+
if (stripe && Number(p.price) > 0) l.push({ centimes: Number(p.price), devise: deviseStripe });
|
|
622
680
|
return l.length ? l : null;
|
|
623
681
|
};
|
|
624
682
|
const suite = suiteSure(url.searchParams.get('suite'));
|
|
@@ -729,7 +787,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
729
787
|
const fin = sub?.currentPeriodEnd && !sub?.stripeSubId && Number(actif?.plan?.price) > 0
|
|
730
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>` : '';
|
|
731
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>`;
|
|
732
|
-
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 }));
|
|
733
791
|
}
|
|
734
792
|
// 0.5 — changer son mot de passe : l'actuel d'abord ; les AUTRES sessions sont fermées, celle-ci rouverte.
|
|
735
793
|
if (chemin === '/compte/mot-de-passe' && methodeConnexion === 'motdepasse') {
|
|
@@ -748,7 +806,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
748
806
|
// 0.5 — partir avec ses données (RGPD, portabilité) : un fichier JSON.
|
|
749
807
|
if (chemin === '/compte/export' && methode === 'GET') {
|
|
750
808
|
const u = await d.findOne(S.User, { email: String(qui.utilisateur.courriel) });
|
|
751
|
-
const vue = await vueEspace(c);
|
|
809
|
+
const vue = await vueEspace(c, lang);
|
|
752
810
|
const donnees = {
|
|
753
811
|
exporteLe: new Date().toISOString(), service: config.marque.nom,
|
|
754
812
|
utilisateur: { courriel: u?.email, prenom: u?.firstName, nom: u?.lastName, langue: u?.locale, creeLe: u?.createdAt },
|
|
@@ -788,8 +846,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
788
846
|
const f = await formulaire();
|
|
789
847
|
try {
|
|
790
848
|
const k = await comptes.cles.creer(String(c.id), { label: String(f.libelle ?? '').slice(0, 60), portees: {} });
|
|
791
|
-
return envoyer(200, page({ actif: '/compte', titre: t('espace.compte'), contenu: ui.espace({ lang, vue: await vueEspace(c), cleNeuve: k.full }) }));
|
|
792
|
-
} 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 }) })); }
|
|
793
851
|
}
|
|
794
852
|
if (chemin === '/compte/cles/revoquer' && methode === 'POST') {
|
|
795
853
|
await comptes.cles.revoquer(String(c.id), String((await formulaire()).prefix ?? ''));
|
|
@@ -894,6 +952,8 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
894
952
|
}
|
|
895
953
|
|
|
896
954
|
return {
|
|
955
|
+
// 0.6 — les moyens réellement proposés (après le contrôle du vendeur de chaque devise)
|
|
956
|
+
moyens: [...(stripe ? ['stripe'] : []), ...Object.keys(banques)],
|
|
897
957
|
handler: (req, res) => handler(req, res).catch((e) => { journal.error(e); if (!res.headersSent) { res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' }); res.end('Erreur interne'); } }),
|
|
898
958
|
catalogue, catalogueStripe, dialect: d, schemas: S, comptes, facturation, confirmerAdmin, reprendrePaiements,
|
|
899
959
|
/** Fixe le mot de passe d'un compte depuis le serveur (mot de passe perdu) ; lève si la règle le refuse. */
|