@mostajs/subscriptions-plan 0.4.0 → 0.6.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 +28 -0
- package/README.md +12 -0
- package/dist/lib/billing.d.ts +149 -0
- package/dist/lib/billing.d.ts.map +1 -0
- package/dist/lib/billing.js +414 -0
- package/dist/lib/billing.js.map +1 -0
- package/dist/schemas/subscription.schema.d.ts.map +1 -1
- package/dist/schemas/subscription.schema.js +3 -0
- package/dist/schemas/subscription.schema.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/llms.txt +18 -0
- package/package.json +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
## 0.6.0 — 2026-10-02
|
|
7
|
+
|
|
8
|
+
- **Portail client Stripe** : `openPortal({ accountId, returnUrl, locale? })` — le client change de palier ou de
|
|
9
|
+
carte, voit ses factures, résilie ; seulement pour un compte qui a déjà un client Stripe.
|
|
10
|
+
- **Pause et reprise** : `customer.subscription.paused` / `resumed` appliqués ; `paused` rejoint les statuts qui
|
|
11
|
+
ne servent plus le palier payant ; l'ordre d'arrivée est respecté.
|
|
12
|
+
- **Le plan suit le TARIF** de l'abonnement (le portail change le tarif sans toucher aux métadonnées) ; repli
|
|
13
|
+
sur les métadonnées pour un tarif inconnu.
|
|
14
|
+
- Essais T-SP-12 à 14, écrits avant le code, chacun confronté à un sabotage.
|
|
15
|
+
## 0.5.0 — 2026-10-01
|
|
16
|
+
|
|
17
|
+
### Ajouté (additif, **non-breaking**) — la facturation de mdseal, remontée dans le module
|
|
18
|
+
- **`createBilling({ dialect, resources, freePlan })`** : ressources de quota **déclarées** par l'application
|
|
19
|
+
(`{ clé: { limit: 'champ du plan', period: 'day' | 'month' } }`) — fin du domaine fermé à trois.
|
|
20
|
+
`check` rend un verdict **permanent** (pas d'abonnement, plan muet ou à 0 : changer de plan) ou **temporaire**
|
|
21
|
+
(forfait épuisé, `retryAfterSeconds`). `consume` réserve **atomiquement** (borne exacte dans le processus,
|
|
22
|
+
jamais dépassée entre processus), `release` rend. `setPlan` clôt avant d'ouvrir. `updatePlanLimits` refuse une
|
|
23
|
+
**échelle inversée** (incident mdseal du 26/09).
|
|
24
|
+
- **`checkLimits`** (verdict pur, pour le palier anonyme), **`assertMonotonicPlans`**, **`secondsUntilReset`**.
|
|
25
|
+
- **`createStripeSale({ billing, stripe, webhookSecret, customers, metadataKeys, urls })`** :
|
|
26
|
+
`openCheckout` (client Stripe créé une fois par un port de l'application ; métadonnées sur la session **et**
|
|
27
|
+
`subscription_data`) ; `handleWebhook(corpsBrut, signature)` — 400/503/500/200, **idempotent par `event.id`**
|
|
28
|
+
(journal `BillingEvent`), **ordre d'arrivée** respecté (un événement plus ancien que le dernier appliqué est
|
|
29
|
+
ignoré ; un `updated` tardif après un `deleted` ne ré-abonne pas), impayé → plan gratuit, facture en échec puis
|
|
30
|
+
payée → « payée ».
|
|
31
|
+
- Schémas `UsageCounter`, `BillingEvent` ; `Subscription` gagne `stripeEventAt`, `stripeEndedAt`.
|
|
32
|
+
|
|
5
33
|
## 0.4.0 — 2026-06-10
|
|
6
34
|
|
|
7
35
|
### Ajouté (additif, **non-breaking**)
|
package/README.md
CHANGED
|
@@ -44,6 +44,18 @@ Additif et **non-breaking** : sans `scope`, le comportement est identique aux ve
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
+
## Billing engine *(v0.5.0)*
|
|
48
|
+
|
|
49
|
+
Declared quota resources (`{ key: { limit, period: 'day' | 'month' } }`), permanent vs temporary verdicts
|
|
50
|
+
with `Retry-After`, an atomic counter, a monotonic tier scale, and a Stripe sale whose webhook is idempotent
|
|
51
|
+
by `event.id` and robust to out-of-order delivery. See `llms.txt` § 0.5.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const billing = createBilling({ dialect, freePlan: 'free', resources: { signature: { limit: 'signaturesPerMonth', period: 'month' } } })
|
|
55
|
+
const v = await billing.consume(accountId, 'signature')
|
|
56
|
+
const sale = createStripeSale({ billing, stripe, webhookSecret, customers, urls })
|
|
57
|
+
```
|
|
58
|
+
|
|
47
59
|
## Install
|
|
48
60
|
|
|
49
61
|
```bash
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { EntitySchema } from '@mostajs/data-plug';
|
|
2
|
+
type Dialect = any;
|
|
3
|
+
/** Le compteur : une ligne par compte × ressource × période (jour `YYYY-MM-DD`, mois `YYYY-MM`). */
|
|
4
|
+
export declare const UsageCounterSchema: EntitySchema;
|
|
5
|
+
/** Le journal des événements de paiement déjà reçus : l'idempotence par `event.id`. */
|
|
6
|
+
export declare const BillingEventSchema: EntitySchema;
|
|
7
|
+
/** Tous les schémas qu'emploie la facturation 0.5 (à initialiser avec le schéma `Account` de l'hôte). */
|
|
8
|
+
export declare function billingSchemas(): EntitySchema[];
|
|
9
|
+
export type Period = 'day' | 'month';
|
|
10
|
+
export interface ResourceDef {
|
|
11
|
+
limit: string;
|
|
12
|
+
period: Period;
|
|
13
|
+
}
|
|
14
|
+
export interface Verdict {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
/** `true` : attendre n'y changera rien (pas d'abonnement, plan muet ou à 0) — changer de plan. */
|
|
17
|
+
permanent?: boolean;
|
|
18
|
+
resource?: string;
|
|
19
|
+
current: number;
|
|
20
|
+
/** -1 = sans limite. */
|
|
21
|
+
limit: number;
|
|
22
|
+
period: Period;
|
|
23
|
+
plan?: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
/** Pour l'en-tête `Retry-After` d'un refus TEMPORAIRE. */
|
|
26
|
+
retryAfterSeconds?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const UNLIMITED = -1;
|
|
29
|
+
/** Secondes jusqu'à la réouverture : minuit UTC (jour) ou le 1er du mois suivant (mois). */
|
|
30
|
+
export declare function secondsUntilReset(period: Period, now?: Date): number;
|
|
31
|
+
/**
|
|
32
|
+
* Le verdict PUR : des limites, une ressource, une consommation. Sert au compte comme au demandeur
|
|
33
|
+
* sans compte (palier anonyme). Un plan MUET sur une ressource ne l'autorise pas : le silence d'un
|
|
34
|
+
* catalogue ne vaut pas permission.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkLimits(limits: Record<string, any>, def: ResourceDef, current: number, now?: Date): Verdict;
|
|
37
|
+
export declare class BillingError extends Error {
|
|
38
|
+
code: string;
|
|
39
|
+
constructor(code: string, message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* L'échelle monte : rangés par `sortOrder`, un palier ne donne jamais MOINS que celui d'en dessous,
|
|
43
|
+
* sur aucune des clés. -1 (sans limite) vaut l'infini.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertMonotonicPlans(plans: Array<{
|
|
46
|
+
slug: string;
|
|
47
|
+
sortOrder?: number;
|
|
48
|
+
limits: any;
|
|
49
|
+
}>, keys: string[]): void;
|
|
50
|
+
export interface BillingOptions {
|
|
51
|
+
dialect: Dialect;
|
|
52
|
+
resources: Record<string, ResourceDef>;
|
|
53
|
+
/** Le plan de repli (résiliation, impayé). */
|
|
54
|
+
freePlan?: string;
|
|
55
|
+
clock?: () => Date;
|
|
56
|
+
}
|
|
57
|
+
export declare function createBilling(opts: BillingOptions): {
|
|
58
|
+
/** La base où vivent plans, abonnements, compteurs et factures. */
|
|
59
|
+
dialect: any;
|
|
60
|
+
resources: Readonly<{
|
|
61
|
+
[x: string]: ResourceDef;
|
|
62
|
+
}>;
|
|
63
|
+
freePlan: string;
|
|
64
|
+
activePlan: (accountId: string) => Promise<{
|
|
65
|
+
subscription: any;
|
|
66
|
+
plan: any;
|
|
67
|
+
limits: Record<string, any>;
|
|
68
|
+
} | null>;
|
|
69
|
+
check: (accountId: string, resource: string) => Promise<Verdict>;
|
|
70
|
+
setPlan: (accountId: string, slug: string, extra?: Record<string, unknown>) => Promise<any>;
|
|
71
|
+
/** Réserve `n` unités : atomique, ne dépasse jamais la borne. À rendre (`release`) si le travail échoue. */
|
|
72
|
+
consume(accountId: string, resource: string, n?: number): Promise<Verdict>;
|
|
73
|
+
release(accountId: string, resource: string, n?: number): Promise<void>;
|
|
74
|
+
/** La consommation de la période courante, par ressource déclarée. */
|
|
75
|
+
usage(accountId: string): Promise<Record<string, number>>;
|
|
76
|
+
/** Modifier les limites d'un plan — refusé si l'échelle s'inverse (tous les plans, vendus ou non). */
|
|
77
|
+
updatePlanLimits(slug: string, limits: Record<string, any>): Promise<any>;
|
|
78
|
+
};
|
|
79
|
+
export type Billing = ReturnType<typeof createBilling>;
|
|
80
|
+
/** Le port de l'application : où elle range l'identifiant client Stripe de ses comptes. */
|
|
81
|
+
export interface CustomerPort {
|
|
82
|
+
get(accountId: string): Promise<string | null>;
|
|
83
|
+
set(accountId: string, customerId: string): Promise<void>;
|
|
84
|
+
describe(accountId: string): Promise<{
|
|
85
|
+
email?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
}>;
|
|
88
|
+
accountOf?(customerId: string): Promise<string | null>;
|
|
89
|
+
}
|
|
90
|
+
export interface StripeSaleOptions {
|
|
91
|
+
billing: Billing;
|
|
92
|
+
/** Une instance `stripe` (ou tout objet de même forme : customers, checkout.sessions, webhooks). */
|
|
93
|
+
stripe: any;
|
|
94
|
+
webhookSecret?: string;
|
|
95
|
+
customers: CustomerPort;
|
|
96
|
+
/** Les clés de métadonnées qui relient Stripe à l'application. */
|
|
97
|
+
metadataKeys?: {
|
|
98
|
+
account: string;
|
|
99
|
+
plan: string;
|
|
100
|
+
};
|
|
101
|
+
urls: {
|
|
102
|
+
success?: string;
|
|
103
|
+
cancel?: string;
|
|
104
|
+
};
|
|
105
|
+
/** Statuts Stripe d'un abonnement qui ne paie plus : retour au plan gratuit. */
|
|
106
|
+
unpaidStatuses?: string[];
|
|
107
|
+
}
|
|
108
|
+
export declare function createStripeSale(opts: StripeSaleOptions): {
|
|
109
|
+
openCheckout: ({ accountId, planSlug, locale }: {
|
|
110
|
+
accountId: string;
|
|
111
|
+
planSlug: string;
|
|
112
|
+
locale?: string;
|
|
113
|
+
}) => Promise<{
|
|
114
|
+
ok: false;
|
|
115
|
+
reason: string;
|
|
116
|
+
url?: undefined;
|
|
117
|
+
id?: undefined;
|
|
118
|
+
} | {
|
|
119
|
+
ok: true;
|
|
120
|
+
url: string;
|
|
121
|
+
id: string;
|
|
122
|
+
reason?: undefined;
|
|
123
|
+
}>;
|
|
124
|
+
openPortal: ({ accountId, returnUrl, locale }: {
|
|
125
|
+
accountId: string;
|
|
126
|
+
returnUrl: string;
|
|
127
|
+
locale?: string;
|
|
128
|
+
}) => Promise<{
|
|
129
|
+
ok: false;
|
|
130
|
+
reason: string;
|
|
131
|
+
url?: undefined;
|
|
132
|
+
} | {
|
|
133
|
+
ok: true;
|
|
134
|
+
url: string;
|
|
135
|
+
reason?: undefined;
|
|
136
|
+
}>;
|
|
137
|
+
/**
|
|
138
|
+
* Vérifie et applique un événement. `rawBody` : le corps EXACT (Buffer), jamais ré-sérialisé.
|
|
139
|
+
* 400 signature refusée (Stripe cesse de réessayer) · 503 non configuré · 500 échec d'application
|
|
140
|
+
* (Stripe réessaiera ; l'événement n'est pas marqué reçu) · 200 sinon, rejeu compris.
|
|
141
|
+
*/
|
|
142
|
+
handleWebhook(rawBody: Buffer | string, signature: string): Promise<{
|
|
143
|
+
status: number;
|
|
144
|
+
result?: string;
|
|
145
|
+
type?: string;
|
|
146
|
+
}>;
|
|
147
|
+
};
|
|
148
|
+
export {};
|
|
149
|
+
//# sourceMappingURL=billing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../src/lib/billing.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAKtD,KAAK,OAAO,GAAG,GAAG,CAAA;AAElB,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,EAAE,YAYhB,CAAA;AAEjB,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,EAAE,YAWhB,CAAA;AAEjB,yGAAyG;AACzG,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C;AAED,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,OAAO,CAAA;AACpC,MAAM,WAAW,WAAW;IAAyD,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAEpH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,kGAAkG;IAClG,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,SAAS,KAAK,CAAA;AAE3B,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAKhF;AAKD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAW3H;AAED,qBAAa,YAAa,SAAQ,KAAK;IAClB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CACjD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAY1H;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACtC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc;IAsE9C,mEAAmE;;;;;;4BA1ChC,MAAM;;;;;uBAoBX,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;yBAQ/C,MAAM,QAAQ,MAAM,UAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAsB9E,4GAA4G;uBACnF,MAAM,YAAY,MAAM,eAAU,OAAO,CAAC,OAAO,CAAC;uBAelD,MAAM,YAAY,MAAM,eAAU,OAAO,CAAC,IAAI,CAAC;IAQxE,sEAAsE;qBAC/C,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAM/D,sGAAsG;2BACzE,MAAM,UAAU,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;EASnE;AAED,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAA;AAItD,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC9C,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvE,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACvD;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAA;IAChB,oGAAoG;IACpG,MAAM,EAAE,GAAG,CAAA;IACX,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,YAAY,CAAA;IACvB,kEAAkE;IAClE,YAAY,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAChD,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB;oDAOO;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;aAsBzE,MAAM;YAAc,MAAM;;;mDAOV;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;aAKzE,MAAM;;;IA0FhD;;;;OAIG;2BAC0B,MAAM,GAAG,MAAM,aAAa,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;EAsBhI"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// @mostajs/subscriptions-plan 0.5 — quotas DÉCLARÉS, compteur atomique, paliers, vente et webhooks Stripe.
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
//
|
|
4
|
+
// Ce fichier remonte dans le module ce que mdseal fait en production (`plans.mjs`, `acces.mjs`,
|
|
5
|
+
// `administration.mjs`, `vente.mjs`) et ferme ce que l'audit du 01/10/2026 y a trouvé :
|
|
6
|
+
//
|
|
7
|
+
// - le domaine de ressources était FERMÉ à trois (`requests | projects | apiKeys`) : la signature
|
|
8
|
+
// de mdseal n'y entrait pas, et l'application avait réécrit le contrôle à côté. Les ressources
|
|
9
|
+
// sont maintenant DÉCLARÉES par l'application : { clé, champ de limite du plan, période } ;
|
|
10
|
+
// - deux refus qui ne se corrigent pas pareil : PERMANENT (« ton plan ne comprend pas ça » : en
|
|
11
|
+
// changer) et TEMPORAIRE (« forfait épuisé » : il rouvre, et la date se calcule, donc se dit) ;
|
|
12
|
+
// - le compteur se lisait puis s'écrivait : deux requêtes simultanées passaient toutes deux sous
|
|
13
|
+
// la borne. Il s'INCRÉMENTE d'abord, atomiquement, et rend l'unité s'il a dépassé ;
|
|
14
|
+
// - l'échelle des paliers MONTE toujours (incident mdseal du 26/09 : l'anonyme signait, le gratuit
|
|
15
|
+
// non — s'inscrire faisait perdre un droit) : contrôlée ici, pas dans l'écran ;
|
|
16
|
+
// - webhooks : idempotents par `event.id` ; un événement plus ancien que le dernier appliqué est
|
|
17
|
+
// ignoré, et un `updated` tardif après un `deleted` ne ré-abonne plus ; une facture en échec
|
|
18
|
+
// puis payée passe à « payée » (elle restait figée en échec).
|
|
19
|
+
import { PlanSchema } from '../schemas/plan.schema.js';
|
|
20
|
+
import { SubscriptionSchema } from '../schemas/subscription.schema.js';
|
|
21
|
+
import { InvoiceSchema } from '../schemas/invoice.schema.js';
|
|
22
|
+
/** Le compteur : une ligne par compte × ressource × période (jour `YYYY-MM-DD`, mois `YYYY-MM`). */
|
|
23
|
+
export const UsageCounterSchema = {
|
|
24
|
+
name: 'UsageCounter',
|
|
25
|
+
collection: 'usage_counters',
|
|
26
|
+
timestamps: true,
|
|
27
|
+
fields: {
|
|
28
|
+
account: { type: 'string', required: true },
|
|
29
|
+
resource: { type: 'string', required: true },
|
|
30
|
+
period: { type: 'string', required: true },
|
|
31
|
+
count: { type: 'number', default: 0 },
|
|
32
|
+
},
|
|
33
|
+
relations: {},
|
|
34
|
+
indexes: [{ fields: { account: 'asc', resource: 'asc', period: 'asc' }, unique: true }],
|
|
35
|
+
};
|
|
36
|
+
/** Le journal des événements de paiement déjà reçus : l'idempotence par `event.id`. */
|
|
37
|
+
export const BillingEventSchema = {
|
|
38
|
+
name: 'BillingEvent',
|
|
39
|
+
collection: 'billing_events',
|
|
40
|
+
timestamps: true,
|
|
41
|
+
fields: {
|
|
42
|
+
eventId: { type: 'string', required: true },
|
|
43
|
+
type: { type: 'string' },
|
|
44
|
+
result: { type: 'string' },
|
|
45
|
+
},
|
|
46
|
+
relations: {},
|
|
47
|
+
indexes: [{ fields: { eventId: 'asc' }, unique: true }],
|
|
48
|
+
};
|
|
49
|
+
/** Tous les schémas qu'emploie la facturation 0.5 (à initialiser avec le schéma `Account` de l'hôte). */
|
|
50
|
+
export function billingSchemas() {
|
|
51
|
+
return [PlanSchema, SubscriptionSchema, InvoiceSchema, UsageCounterSchema, BillingEventSchema];
|
|
52
|
+
}
|
|
53
|
+
export const UNLIMITED = -1;
|
|
54
|
+
/** Secondes jusqu'à la réouverture : minuit UTC (jour) ou le 1er du mois suivant (mois). */
|
|
55
|
+
export function secondsUntilReset(period, now = new Date()) {
|
|
56
|
+
const next = period === 'month'
|
|
57
|
+
? Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)
|
|
58
|
+
: Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1);
|
|
59
|
+
return Math.max(1, Math.ceil((next - now.getTime()) / 1000));
|
|
60
|
+
}
|
|
61
|
+
const periodKey = (period, now) => (period === 'month' ? now.toISOString().slice(0, 7) : now.toISOString().slice(0, 10));
|
|
62
|
+
const limitsOf = (plan) => (typeof plan?.limits === 'string' ? JSON.parse(plan.limits) : (plan?.limits ?? {}));
|
|
63
|
+
/**
|
|
64
|
+
* Le verdict PUR : des limites, une ressource, une consommation. Sert au compte comme au demandeur
|
|
65
|
+
* sans compte (palier anonyme). Un plan MUET sur une ressource ne l'autorise pas : le silence d'un
|
|
66
|
+
* catalogue ne vaut pas permission.
|
|
67
|
+
*/
|
|
68
|
+
export function checkLimits(limits, def, current, now = new Date()) {
|
|
69
|
+
const limit = limits?.[def.limit];
|
|
70
|
+
if (limit === undefined || limit === null || Number(limit) === 0) {
|
|
71
|
+
return { allowed: false, permanent: true, current, limit: 0, period: def.period, reason: 'plan does not include this resource' };
|
|
72
|
+
}
|
|
73
|
+
if (Number(limit) === UNLIMITED)
|
|
74
|
+
return { allowed: true, current, limit: UNLIMITED, period: def.period };
|
|
75
|
+
if (current < Number(limit))
|
|
76
|
+
return { allowed: true, current, limit: Number(limit), period: def.period };
|
|
77
|
+
return {
|
|
78
|
+
allowed: false, permanent: false, current, limit: Number(limit), period: def.period,
|
|
79
|
+
reason: 'quota reached', retryAfterSeconds: secondsUntilReset(def.period, now),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export class BillingError extends Error {
|
|
83
|
+
code;
|
|
84
|
+
constructor(code, message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.code = code;
|
|
87
|
+
this.name = 'BillingError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* L'échelle monte : rangés par `sortOrder`, un palier ne donne jamais MOINS que celui d'en dessous,
|
|
92
|
+
* sur aucune des clés. -1 (sans limite) vaut l'infini.
|
|
93
|
+
*/
|
|
94
|
+
export function assertMonotonicPlans(plans, keys) {
|
|
95
|
+
const v = (x) => (Number(x) === UNLIMITED ? Infinity : Number(x ?? 0));
|
|
96
|
+
const tries = [...plans].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
|
97
|
+
for (let i = 1; i < tries.length; i++) {
|
|
98
|
+
const bas = limitsOf(tries[i - 1]);
|
|
99
|
+
const haut = limitsOf(tries[i]);
|
|
100
|
+
for (const k of keys) {
|
|
101
|
+
if (v(haut[k]) < v(bas[k])) {
|
|
102
|
+
throw new BillingError('PLAN_SCALE_INVERTED', `« ${tries[i].slug} » donnerait ${haut[k]} en ${k}, moins que « ${tries[i - 1].slug} » (${bas[k]}) : un palier supérieur ne peut pas donner moins.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createBilling(opts) {
|
|
108
|
+
const d = opts.dialect;
|
|
109
|
+
const clock = opts.clock ?? (() => new Date());
|
|
110
|
+
const freePlan = opts.freePlan ?? 'free';
|
|
111
|
+
for (const [k, r] of Object.entries(opts.resources ?? {})) {
|
|
112
|
+
if (!r?.limit || !['day', 'month'].includes(r.period))
|
|
113
|
+
throw new BillingError('RESOURCE_INVALID', `ressource « ${k} » : { limit, period: 'day' | 'month' } attendu`);
|
|
114
|
+
}
|
|
115
|
+
const resources = { ...opts.resources };
|
|
116
|
+
const def = (resource) => {
|
|
117
|
+
const r = resources[resource];
|
|
118
|
+
if (!r)
|
|
119
|
+
throw new BillingError('RESOURCE_UNKNOWN', `ressource non déclarée : ${resource} (déclarées : ${Object.keys(resources).join(', ')})`);
|
|
120
|
+
return r;
|
|
121
|
+
};
|
|
122
|
+
// Sérialisation par clé, dans le processus : rend la borne EXACTE ici. Entre processus, l'incrément
|
|
123
|
+
// atomique garantit qu'on ne la DÉPASSE jamais — mais une rafale concurrente peut alors être
|
|
124
|
+
// refusée en entier (chacun voit le compteur gonflé par les autres avant leur rendu). Mesuré le
|
|
125
|
+
// 01/10/2026 : 20 demandes simultanées sans sérialisation, borne 3 → 0 accordée. Un service à
|
|
126
|
+
// plusieurs processus doit donc router un même compte vers un même processus.
|
|
127
|
+
const files = new Map();
|
|
128
|
+
function enFile(cle, f) {
|
|
129
|
+
const p = (files.get(cle) ?? Promise.resolve()).then(f, f);
|
|
130
|
+
const fin = p.catch(() => { });
|
|
131
|
+
files.set(cle, fin);
|
|
132
|
+
fin.then(() => { if (files.get(cle) === fin)
|
|
133
|
+
files.delete(cle); });
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
async function activePlan(accountId) {
|
|
137
|
+
const subs = await d.find(SubscriptionSchema, { account: accountId, status: 'active' });
|
|
138
|
+
if (!subs.length)
|
|
139
|
+
return null;
|
|
140
|
+
const plan = await d.findById(PlanSchema, String(subs[0].plan ?? subs[0].planId));
|
|
141
|
+
return plan ? { subscription: subs[0], plan, limits: limitsOf(plan) } : null;
|
|
142
|
+
}
|
|
143
|
+
async function ligneCompteur(accountId, resource, period) {
|
|
144
|
+
const f = { account: accountId, resource, period };
|
|
145
|
+
const l = await d.findOne(UsageCounterSchema, f);
|
|
146
|
+
if (l)
|
|
147
|
+
return l;
|
|
148
|
+
try {
|
|
149
|
+
return await d.create(UsageCounterSchema, { ...f, count: 0 });
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return d.findOne(UsageCounterSchema, f);
|
|
153
|
+
} // créée en parallèle
|
|
154
|
+
}
|
|
155
|
+
async function courant(accountId, resource) {
|
|
156
|
+
const r = def(resource);
|
|
157
|
+
const l = await d.findOne(UsageCounterSchema, { account: accountId, resource, period: periodKey(r.period, clock()) });
|
|
158
|
+
return Number(l?.count ?? 0);
|
|
159
|
+
}
|
|
160
|
+
async function check(accountId, resource) {
|
|
161
|
+
const r = def(resource);
|
|
162
|
+
const actif = await activePlan(accountId);
|
|
163
|
+
if (!actif)
|
|
164
|
+
return { allowed: false, permanent: true, resource, current: 0, limit: 0, period: r.period, reason: 'no active subscription' };
|
|
165
|
+
return { ...checkLimits(actif.limits, r, await courant(accountId, resource), clock()), resource, plan: actif.plan.slug };
|
|
166
|
+
}
|
|
167
|
+
/** Clôt avant d'ouvrir : jamais deux abonnements actifs. Sérialisé par compte. */
|
|
168
|
+
function setPlan(accountId, slug, extra = {}) {
|
|
169
|
+
return enFile(`plan:${accountId}`, async () => {
|
|
170
|
+
const plan = await d.findOne(PlanSchema, { slug });
|
|
171
|
+
if (!plan)
|
|
172
|
+
throw new BillingError('PLAN_UNKNOWN', `plan inconnu : ${slug}`);
|
|
173
|
+
await d.updateMany(SubscriptionSchema, { account: accountId, status: 'active' }, { status: 'canceled' });
|
|
174
|
+
const stripeSubId = extra.stripeSubId;
|
|
175
|
+
const existante = stripeSubId ? await d.findOne(SubscriptionSchema, { stripeSubId }) : null;
|
|
176
|
+
if (existante)
|
|
177
|
+
await d.update(SubscriptionSchema, String(existante.id), { ...extra, plan: plan.id, status: 'active' });
|
|
178
|
+
else
|
|
179
|
+
await d.create(SubscriptionSchema, { ...extra, account: accountId, plan: plan.id, status: 'active' });
|
|
180
|
+
return plan;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
/** La base où vivent plans, abonnements, compteurs et factures. */
|
|
185
|
+
dialect: d,
|
|
186
|
+
resources: Object.freeze({ ...resources }),
|
|
187
|
+
freePlan,
|
|
188
|
+
activePlan,
|
|
189
|
+
check,
|
|
190
|
+
setPlan,
|
|
191
|
+
/** Réserve `n` unités : atomique, ne dépasse jamais la borne. À rendre (`release`) si le travail échoue. */
|
|
192
|
+
async consume(accountId, resource, n = 1) {
|
|
193
|
+
const r = def(resource);
|
|
194
|
+
return enFile(`q:${accountId}|${resource}`, async () => {
|
|
195
|
+
const v = await check(accountId, resource);
|
|
196
|
+
if (!v.allowed)
|
|
197
|
+
return v;
|
|
198
|
+
const l = await ligneCompteur(accountId, resource, periodKey(r.period, clock()));
|
|
199
|
+
const apres = Number((await d.increment(UsageCounterSchema, String(l.id), 'count', n))?.count ?? 0);
|
|
200
|
+
if (v.limit !== UNLIMITED && apres > v.limit) {
|
|
201
|
+
await d.increment(UsageCounterSchema, String(l.id), 'count', -n);
|
|
202
|
+
return { ...v, allowed: false, permanent: false, current: v.limit, reason: 'quota reached', retryAfterSeconds: secondsUntilReset(r.period, clock()) };
|
|
203
|
+
}
|
|
204
|
+
return { ...v, current: apres };
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
async release(accountId, resource, n = 1) {
|
|
208
|
+
const r = def(resource);
|
|
209
|
+
await enFile(`q:${accountId}|${resource}`, async () => {
|
|
210
|
+
const l = await d.findOne(UsageCounterSchema, { account: accountId, resource, period: periodKey(r.period, clock()) });
|
|
211
|
+
if (l && Number(l.count) > 0)
|
|
212
|
+
await d.increment(UsageCounterSchema, String(l.id), 'count', -Math.min(n, Number(l.count)));
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
/** La consommation de la période courante, par ressource déclarée. */
|
|
216
|
+
async usage(accountId) {
|
|
217
|
+
const out = {};
|
|
218
|
+
for (const k of Object.keys(resources))
|
|
219
|
+
out[k] = await courant(accountId, k);
|
|
220
|
+
return out;
|
|
221
|
+
},
|
|
222
|
+
/** Modifier les limites d'un plan — refusé si l'échelle s'inverse (tous les plans, vendus ou non). */
|
|
223
|
+
async updatePlanLimits(slug, limits) {
|
|
224
|
+
const tous = await d.find(PlanSchema, {});
|
|
225
|
+
const cible = tous.find((p) => p.slug === slug);
|
|
226
|
+
if (!cible)
|
|
227
|
+
throw new BillingError('PLAN_UNKNOWN', `plan inconnu : ${slug}`);
|
|
228
|
+
const nouvelles = { ...limitsOf(cible), ...limits };
|
|
229
|
+
assertMonotonicPlans(tous.map((p) => (p.slug === slug ? { ...p, limits: nouvelles } : p)), Object.values(resources).map((r) => r.limit));
|
|
230
|
+
return d.update(PlanSchema, String(cible.id), { limits: nouvelles });
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function createStripeSale(opts) {
|
|
235
|
+
const { billing, stripe, customers } = opts;
|
|
236
|
+
const K = opts.metadataKeys ?? { account: 'account', plan: 'plan' };
|
|
237
|
+
// 0.6 : « paused » — une pause décidée dans le portail ou par l'exploitant ne sert plus le palier payant.
|
|
238
|
+
const IMPAYES = new Set(opts.unpaidStatuses ?? ['canceled', 'unpaid', 'incomplete_expired', 'paused']);
|
|
239
|
+
const dialect = () => billing.dialect;
|
|
240
|
+
async function openCheckout({ accountId, planSlug, locale }) {
|
|
241
|
+
const plan = await dialect().findOne(PlanSchema, { slug: planSlug });
|
|
242
|
+
if (!plan)
|
|
243
|
+
return { ok: false, reason: `plan inconnu : ${planSlug}` };
|
|
244
|
+
if (!plan.price)
|
|
245
|
+
return { ok: false, reason: 'plan gratuit : il ne passe pas par le paiement' };
|
|
246
|
+
if (!plan.stripePriceId)
|
|
247
|
+
return { ok: false, reason: `le plan « ${planSlug} » n’a pas de tarif Stripe` };
|
|
248
|
+
let client = await customers.get(accountId);
|
|
249
|
+
if (!client) {
|
|
250
|
+
// UNE fois par compte : sinon chaque tentative fabrique un client de plus dans Stripe.
|
|
251
|
+
const qui = await customers.describe(accountId);
|
|
252
|
+
client = (await stripe.customers.create({ email: qui.email, name: qui.name, metadata: { [K.account]: accountId } })).id;
|
|
253
|
+
await customers.set(accountId, client);
|
|
254
|
+
}
|
|
255
|
+
const metadata = { [K.account]: accountId, [K.plan]: planSlug };
|
|
256
|
+
const s = await stripe.checkout.sessions.create({
|
|
257
|
+
mode: 'subscription', customer: client,
|
|
258
|
+
line_items: [{ price: plan.stripePriceId, quantity: 1 }],
|
|
259
|
+
...(locale ? { locale } : {}),
|
|
260
|
+
success_url: opts.urls.success, cancel_url: opts.urls.cancel,
|
|
261
|
+
// Sur la session ET sur l'abonnement : la session vit quelques heures, l'abonnement des années,
|
|
262
|
+
// et c'est lui que porteront renouvellements et résiliations.
|
|
263
|
+
metadata, subscription_data: { metadata },
|
|
264
|
+
});
|
|
265
|
+
return { ok: true, url: s.url, id: s.id };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 0.6 — le PORTAIL client de Stripe : changer de palier ou de carte, voir ses factures, résilier. Seulement
|
|
269
|
+
* pour un compte qui a déjà un client Stripe (il a ouvert un paiement) : le portail n'en crée jamais.
|
|
270
|
+
*/
|
|
271
|
+
async function openPortal({ accountId, returnUrl, locale }) {
|
|
272
|
+
const client = await customers.get(accountId);
|
|
273
|
+
if (!client)
|
|
274
|
+
return { ok: false, reason: 'aucun abonnement à gérer : ce compte n’a jamais payé' };
|
|
275
|
+
if (!stripe.billingPortal?.sessions?.create)
|
|
276
|
+
return { ok: false, reason: 'portail client indisponible' };
|
|
277
|
+
const s = await stripe.billingPortal.sessions.create({ customer: client, return_url: returnUrl, ...(locale ? { locale } : {}) });
|
|
278
|
+
return { ok: true, url: s.url };
|
|
279
|
+
}
|
|
280
|
+
/** 0.6 — le plan d'un abonnement se lit sur son TARIF (le portail le change sans toucher aux métadonnées). */
|
|
281
|
+
async function planDuTarif(o) {
|
|
282
|
+
const prix = o?.items?.data?.[0]?.price;
|
|
283
|
+
const id = typeof prix === 'string' ? prix : prix?.id;
|
|
284
|
+
if (!id)
|
|
285
|
+
return null;
|
|
286
|
+
const p = await dialect().findOne(PlanSchema, { stripePriceId: id });
|
|
287
|
+
return p ? String(p.slug) : null;
|
|
288
|
+
}
|
|
289
|
+
async function compteDe(o) {
|
|
290
|
+
const direct = o?.metadata?.[K.account] ?? o?.subscription_details?.metadata?.[K.account];
|
|
291
|
+
if (direct)
|
|
292
|
+
return String(direct);
|
|
293
|
+
const sub = typeof o?.subscription === 'string' ? await dialect().findOne(SubscriptionSchema, { stripeSubId: o.subscription }) : null;
|
|
294
|
+
if (sub)
|
|
295
|
+
return String(sub.account);
|
|
296
|
+
const client = typeof o?.customer === 'string' ? o.customer : o?.customer?.id;
|
|
297
|
+
return client && customers.accountOf ? customers.accountOf(client) : null;
|
|
298
|
+
}
|
|
299
|
+
async function appliquer(evt) {
|
|
300
|
+
const o = evt.data?.object ?? {};
|
|
301
|
+
const at = Number(evt.created ?? 0);
|
|
302
|
+
const accountId = await compteDe(o);
|
|
303
|
+
if (!accountId)
|
|
304
|
+
return `ignored (no account): ${evt.type}`;
|
|
305
|
+
const D = dialect();
|
|
306
|
+
switch (evt.type) {
|
|
307
|
+
case 'checkout.session.completed':
|
|
308
|
+
case 'customer.subscription.created':
|
|
309
|
+
case 'customer.subscription.updated':
|
|
310
|
+
case 'customer.subscription.paused':
|
|
311
|
+
case 'customer.subscription.resumed': {
|
|
312
|
+
const stripeSubId = evt.type === 'checkout.session.completed' ? (typeof o.subscription === 'string' ? o.subscription : o.subscription?.id) : o.id;
|
|
313
|
+
const ligne = stripeSubId ? await D.findOne(SubscriptionSchema, { stripeSubId }) : null;
|
|
314
|
+
if (ligne?.stripeEndedAt)
|
|
315
|
+
return `ignored: ${stripeSubId} already ended`;
|
|
316
|
+
if (ligne && Number(ligne.stripeEventAt ?? 0) > at)
|
|
317
|
+
return `ignored: older than last applied event (${stripeSubId})`;
|
|
318
|
+
const statut = evt.type === 'checkout.session.completed' ? 'active' : String(o.status ?? 'active');
|
|
319
|
+
if (IMPAYES.has(statut)) {
|
|
320
|
+
// Seul l'abonnement EN COURS fait redescendre : un ancien abonnement remplacé n'a plus la main.
|
|
321
|
+
if (ligne && ligne.status !== 'active') {
|
|
322
|
+
await D.update(SubscriptionSchema, String(ligne.id), { stripeEventAt: at });
|
|
323
|
+
return `subscription ${stripeSubId} ${statut} (was not active)`;
|
|
324
|
+
}
|
|
325
|
+
await billing.setPlan(accountId, billing.freePlan);
|
|
326
|
+
if (ligne)
|
|
327
|
+
await D.update(SubscriptionSchema, String(ligne.id), { stripeEventAt: at, status: 'canceled' });
|
|
328
|
+
return `account ${accountId} back to ${billing.freePlan} (${statut})`;
|
|
329
|
+
}
|
|
330
|
+
const slug = (evt.type === 'checkout.session.completed' ? null : await planDuTarif(o)) ?? o.metadata?.[K.plan];
|
|
331
|
+
if (!slug)
|
|
332
|
+
return `ignored (no plan in metadata): ${evt.type}`;
|
|
333
|
+
await billing.setPlan(accountId, String(slug), { stripeSubId, stripeEventAt: at });
|
|
334
|
+
return `account ${accountId} on plan ${slug}`;
|
|
335
|
+
}
|
|
336
|
+
case 'customer.subscription.deleted': {
|
|
337
|
+
const ligne = await D.findOne(SubscriptionSchema, { stripeSubId: o.id });
|
|
338
|
+
if (!ligne)
|
|
339
|
+
return `ignored: unknown subscription ${o.id}`;
|
|
340
|
+
if (ligne.stripeEndedAt)
|
|
341
|
+
return 'replay: already ended';
|
|
342
|
+
const etaitActive = ligne.status === 'active';
|
|
343
|
+
await D.update(SubscriptionSchema, String(ligne.id), { stripeEndedAt: at || 1, status: 'canceled' });
|
|
344
|
+
if (etaitActive)
|
|
345
|
+
await billing.setPlan(accountId, billing.freePlan);
|
|
346
|
+
return etaitActive ? `account ${accountId} back to ${billing.freePlan} (ended)` : `subscription ${o.id} ended (was not active)`;
|
|
347
|
+
}
|
|
348
|
+
case 'invoice.paid':
|
|
349
|
+
case 'invoice.payment_failed': {
|
|
350
|
+
const paye = evt.type === 'invoice.paid';
|
|
351
|
+
const existante = await D.findOne(InvoiceSchema, { stripeInvoiceId: o.id });
|
|
352
|
+
const champs = {
|
|
353
|
+
amount: Number(paye ? (o.amount_paid ?? o.amount_due ?? 0) : (o.amount_due ?? 0)),
|
|
354
|
+
currency: o.currency ?? 'eur',
|
|
355
|
+
status: paye ? 'paid' : 'failed',
|
|
356
|
+
...(paye && o.status_transitions?.paid_at ? { paidAt: new Date(o.status_transitions.paid_at * 1000) } : {}),
|
|
357
|
+
...(o.hosted_invoice_url ? { hostedUrl: o.hosted_invoice_url } : {}),
|
|
358
|
+
...(o.invoice_pdf ? { pdfUrl: o.invoice_pdf } : {}),
|
|
359
|
+
};
|
|
360
|
+
if (!existante) {
|
|
361
|
+
await D.create(InvoiceSchema, { ...champs, account: accountId, stripeInvoiceId: o.id });
|
|
362
|
+
return `invoice ${o.id} recorded (${champs.status})`;
|
|
363
|
+
}
|
|
364
|
+
if (existante.status === 'paid')
|
|
365
|
+
return `invoice ${o.id} already paid`; // un échec tardif ne défait pas un paiement
|
|
366
|
+
if (!paye)
|
|
367
|
+
return `invoice ${o.id} still failed`;
|
|
368
|
+
await D.update(InvoiceSchema, String(existante.id), champs);
|
|
369
|
+
return `invoice ${o.id} now paid`;
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
return `no effect: ${evt.type}`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
openCheckout,
|
|
377
|
+
openPortal,
|
|
378
|
+
/**
|
|
379
|
+
* Vérifie et applique un événement. `rawBody` : le corps EXACT (Buffer), jamais ré-sérialisé.
|
|
380
|
+
* 400 signature refusée (Stripe cesse de réessayer) · 503 non configuré · 500 échec d'application
|
|
381
|
+
* (Stripe réessaiera ; l'événement n'est pas marqué reçu) · 200 sinon, rejeu compris.
|
|
382
|
+
*/
|
|
383
|
+
async handleWebhook(rawBody, signature) {
|
|
384
|
+
if (!opts.webhookSecret)
|
|
385
|
+
return { status: 503, result: 'webhook not configured' };
|
|
386
|
+
let evt;
|
|
387
|
+
try {
|
|
388
|
+
evt = stripe.webhooks.constructEvent(rawBody, signature, opts.webhookSecret);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
return { status: 400, result: `signature refused: ${e?.message ?? e}` };
|
|
392
|
+
}
|
|
393
|
+
const D = dialect();
|
|
394
|
+
// Inscrire d'abord : l'index unique fait d'une double livraison simultanée un rejeu.
|
|
395
|
+
let marque;
|
|
396
|
+
try {
|
|
397
|
+
marque = await D.create(BillingEventSchema, { eventId: evt.id, type: evt.type, result: 'pending' });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return { status: 200, type: evt.type, result: 'replay: no effect' };
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const result = await appliquer(evt);
|
|
404
|
+
await D.update(BillingEventSchema, String(marque.id), { result });
|
|
405
|
+
return { status: 200, type: evt.type, result };
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
await D.delete(BillingEventSchema, String(marque.id)).catch(() => { });
|
|
409
|
+
return { status: 500, type: evt.type, result: String(e?.message ?? e) };
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=billing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.js","sourceRoot":"","sources":["../../src/lib/billing.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/subscription.schema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,eAAO,MAAM,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"subscription.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/subscription.schema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,eAAO,MAAM,kBAAkB,EAAE,YAkChC,CAAA"}
|
|
@@ -17,6 +17,9 @@ export const SubscriptionSchema = {
|
|
|
17
17
|
// un par entité scopée. Cf. findActiveSubscription().
|
|
18
18
|
scopeType: { type: 'string', default: 'account' },
|
|
19
19
|
scopeId: { type: 'string' },
|
|
20
|
+
// 0.5 — ordre d'arrivée des webhooks : `created` (s) du dernier événement appliqué, et de la fin.
|
|
21
|
+
stripeEventAt: { type: 'number' },
|
|
22
|
+
stripeEndedAt: { type: 'number' },
|
|
20
23
|
},
|
|
21
24
|
relations: {
|
|
22
25
|
account: { type: 'many-to-one', target: 'Account', required: true },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.schema.js","sourceRoot":"","sources":["../../src/schemas/subscription.schema.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,wCAAwC;AAIxC,MAAM,CAAC,MAAM,kBAAkB,GAAiB;IAC9C,IAAI,EAAE,cAAc;IACpB,UAAU,EAAE,eAAe;IAC3B,UAAU,EAAE,IAAI;IAEhB,MAAM,EAAE;QACN,MAAM,EAAc,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE;QACzE,WAAW,EAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;QACtC,kBAAkB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,gBAAgB,EAAI,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,QAAQ,EAAY,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,QAAQ,EAAY,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,mFAAmF;QACnF,oFAAoF;QACpF,iFAAiF;QACjF,sDAAsD;QACtD,SAAS,EAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE;QAC1D,OAAO,EAAa,EAAE,IAAI,EAAE,QAAQ,EAAE;KACvC;IAED,SAAS,EAAE;QACT,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;QACnE,IAAI,EAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;KACjE;IAED,OAAO,EAAE;QACP,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;QAC7B,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;QAChD,qDAAqD;QACrD,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;KACjD;CACF,CAAA"}
|
|
1
|
+
{"version":3,"file":"subscription.schema.js","sourceRoot":"","sources":["../../src/schemas/subscription.schema.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,wCAAwC;AAIxC,MAAM,CAAC,MAAM,kBAAkB,GAAiB;IAC9C,IAAI,EAAE,cAAc;IACpB,UAAU,EAAE,eAAe;IAC3B,UAAU,EAAE,IAAI;IAEhB,MAAM,EAAE;QACN,MAAM,EAAc,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE;QACzE,WAAW,EAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;QACtC,kBAAkB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,gBAAgB,EAAI,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,QAAQ,EAAY,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,QAAQ,EAAY,EAAE,IAAI,EAAE,MAAM,EAAE;QACpC,mFAAmF;QACnF,oFAAoF;QACpF,iFAAiF;QACjF,sDAAsD;QACtD,SAAS,EAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE;QAC1D,OAAO,EAAa,EAAE,IAAI,EAAE,QAAQ,EAAE;QACtC,kGAAkG;QAClG,aAAa,EAAO,EAAE,IAAI,EAAE,QAAQ,EAAE;QACtC,aAAa,EAAO,EAAE,IAAI,EAAE,QAAQ,EAAE;KACvC;IAED,SAAS,EAAE;QACT,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE;QACnE,IAAI,EAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;KACjE;IAED,OAAO,EAAE;QACP,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;QAC7B,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;QAChD,qDAAqD;QACrD,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;KACjD;CACF,CAAA"}
|
package/dist/server.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export { createSubscriptionHandlers } from './api/subscriptions.route.js';
|
|
|
8
8
|
export { createUsageHandlers } from './api/usage.route.js';
|
|
9
9
|
export { getSchemas, moduleInfo } from './lib/module-info.js';
|
|
10
10
|
export { subscriptionPlanRegistration } from './register.js';
|
|
11
|
+
export { createBilling, createStripeSale, checkLimits, assertMonotonicPlans, secondsUntilReset, billingSchemas, UsageCounterSchema, BillingEventSchema, BillingError, UNLIMITED, } from './lib/billing.js';
|
|
12
|
+
export type { Billing, BillingOptions, ResourceDef, Verdict, Period, CustomerPort, StripeSaleOptions } from './lib/billing.js';
|
|
11
13
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACvG,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAG1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAG1D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAG7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACvG,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAG1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAG1D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAG7D,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAA;AAG5D,OAAO,EACL,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EACrF,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,EAAE,SAAS,GAChF,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/server.js
CHANGED
|
@@ -16,4 +16,6 @@ export { createUsageHandlers } from './api/usage.route.js';
|
|
|
16
16
|
export { getSchemas, moduleInfo } from './lib/module-info.js';
|
|
17
17
|
// Registration
|
|
18
18
|
export { subscriptionPlanRegistration } from './register.js';
|
|
19
|
+
// 0.5 — quotas déclarés, compteur atomique, paliers, vente et webhooks Stripe idempotents
|
|
20
|
+
export { createBilling, createStripeSale, checkLimits, assertMonotonicPlans, secondsUntilReset, billingSchemas, UsageCounterSchema, BillingEventSchema, BillingError, UNLIMITED, } from './lib/billing.js';
|
|
19
21
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wCAAwC;AAExC,eAAe;AACf,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,uBAAuB,CAAA;AAE9B,oBAAoB;AACpB,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,sBAAsB,CAAA;AAE7B,iBAAiB;AACjB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAEhC,kDAAkD;AAClD,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAGvG,eAAe;AACf,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1D,wBAAwB;AACxB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAE7D,eAAe;AACf,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wCAAwC;AAExC,eAAe;AACf,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,UAAU,GACX,MAAM,uBAAuB,CAAA;AAE9B,oBAAoB;AACpB,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,sBAAsB,CAAA;AAE7B,iBAAiB;AACjB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,yBAAyB,CAAA;AAEhC,kDAAkD;AAClD,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAGvG,eAAe;AACf,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1D,wBAAwB;AACxB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAE7D,eAAe;AACf,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAA;AAE5D,0FAA0F;AAC1F,OAAO,EACL,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EACrF,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,EAAE,SAAS,GAChF,MAAM,kBAAkB,CAAA"}
|
package/llms.txt
CHANGED
|
@@ -73,6 +73,24 @@ const q = await checkQuota(dialect, 'acc-1', 'projects')
|
|
|
73
73
|
if (!q.allowed) throw new Error(q.reason)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
## 0.5 — FACTURATION (`@mostajs/subscriptions-plan/server`) — À PRÉFÉRER à checkQuota/incrementUsage
|
|
77
|
+
```js
|
|
78
|
+
const d = await openIsolatedDialect(cfg, [AccountSchema, ...billingSchemas()])
|
|
79
|
+
const billing = createBilling({ dialect: d, freePlan: 'gratuit', resources: {
|
|
80
|
+
conversion: { limit: 'conversionsParJour', period: 'day' },
|
|
81
|
+
signature: { limit: 'signaturesParMois', period: 'month' } } })
|
|
82
|
+
const v = await billing.consume(compteId, 'signature') // AVANT le travail ; billing.release(...) s'il échoue
|
|
83
|
+
if (!v.allowed) return v.permanent ? 403 : 429 + `Retry-After: ${v.retryAfterSeconds}`
|
|
84
|
+
const vente = createStripeSale({ billing, stripe, webhookSecret, customers /* get,set,describe,accountOf */,
|
|
85
|
+
metadataKeys: { account: 'qatrax_compte', plan: 'qatrax_plan' }, urls: { success, cancel } })
|
|
86
|
+
await vente.openCheckout({ accountId, planSlug }) // → { ok, url }
|
|
87
|
+
const r = await vente.handleWebhook(corpsBrutBuffer, req.headers['stripe-signature']) // → { status, result }
|
|
88
|
+
```
|
|
89
|
+
- Verdict : `{ allowed, permanent?, current, limit (-1 = sans limite), period, plan?, reason?, retryAfterSeconds? }`.
|
|
90
|
+
- `checkLimits(limits, def, courant)` : le même verdict pour un demandeur sans compte (palier anonyme : un plan `active: false`, `sortOrder` le plus bas).
|
|
91
|
+
- `updatePlanLimits(slug, limits)` lève `PLAN_SCALE_INVERTED` si un palier supérieur donnerait moins.
|
|
92
|
+
- PIÈGES : le compteur est exact DANS un processus ; entre processus il ne dépasse jamais la borne mais une rafale peut être refusée en entier → router un même compte vers un même processus. Le webhook se vérifie sur le corps BRUT. Le plan n'est changé QUE par le webhook (jamais au retour de la page de paiement).
|
|
93
|
+
|
|
76
94
|
## DÉPEND DE
|
|
77
95
|
- `@mostajs/data-plug` (peer dep `^1.2.4`) — façade ORM : `BaseRepository`, `IDialect`, `EntitySchema`.
|
|
78
96
|
- `@mostajs/config` (peer dep) — résolution env.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/subscriptions-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Subscription plans, billing, invoices and usage tracking for @mostajs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"dev": "tsc --watch",
|
|
23
|
-
"test": "tsx test-scripts/test-unit.ts",
|
|
23
|
+
"test": "npm run build && tsx test-scripts/test-unit.ts && node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjs --json=test-scripts/.out/report.json",
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@mostajs/data-plug": "^1.2.4",
|
|
58
|
+
"@mostajs/mjs-unit": "^0.7.0",
|
|
58
59
|
"@mostajs/orm": "^1.13.1",
|
|
59
60
|
"@mostajs/payment": "^0.4.1",
|
|
60
61
|
"@types/node": "^22.0.0",
|
|
@@ -65,5 +66,15 @@
|
|
|
65
66
|
"funding": {
|
|
66
67
|
"type": "github",
|
|
67
68
|
"url": "https://github.com/sponsors/apolocine"
|
|
69
|
+
},
|
|
70
|
+
"mostajs": {
|
|
71
|
+
"niveau": "N2",
|
|
72
|
+
"nature": "compose",
|
|
73
|
+
"reutilisabilite": "multi-domaine",
|
|
74
|
+
"depend_de": [
|
|
75
|
+
"@mostajs/config",
|
|
76
|
+
"@mostajs/data-plug",
|
|
77
|
+
"@mostajs/payment"
|
|
78
|
+
]
|
|
68
79
|
}
|
|
69
80
|
}
|