@billing-saas/usage-sdk 0.1.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/README.md +152 -0
- package/dist/index.d.mts +281 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +308 -0
- package/dist/index.mjs +278 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @billing-saas/usage-sdk
|
|
2
|
+
|
|
3
|
+
SDK TypeScript pour intégrer le **PU Billing SaaS** dans votre application Next.js.
|
|
4
|
+
|
|
5
|
+
Gérez vos plans, abonnements, checkout Stripe et feature gates en quelques lignes.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @billing-saas/usage-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Démarrage rapide
|
|
14
|
+
|
|
15
|
+
### 1. Créez votre compte tenant
|
|
16
|
+
|
|
17
|
+
Rendez-vous sur [dev.billingsaas.187.124.95.146.sslip.io/tenant/register](http://dev.billingsaas.187.124.95.146.sslip.io/tenant/register), créez votre espace et copiez votre clé API depuis l'onboarding.
|
|
18
|
+
|
|
19
|
+
### 2. Configurez `.env.local`
|
|
20
|
+
|
|
21
|
+
```env
|
|
22
|
+
BILLING_API_URL=http://dev.billingsaas.187.124.95.146.sslip.io/api
|
|
23
|
+
BILLING_API_KEY=bsk_live_...
|
|
24
|
+
NEXT_PUBLIC_APP_URL=https://votre-app.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> ⚠️ **Ne jamais exposer `BILLING_API_KEY` côté client.** Pas de `NEXT_PUBLIC_`.
|
|
28
|
+
|
|
29
|
+
### 3. Initialisez le client (serveur uniquement)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/lib/billing.ts
|
|
33
|
+
import { BillingClient } from '@billing-saas/usage-sdk'
|
|
34
|
+
|
|
35
|
+
export const billing = new BillingClient({
|
|
36
|
+
apiKey: process.env.BILLING_API_KEY!,
|
|
37
|
+
baseUrl: process.env.BILLING_API_URL!,
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 4. Route API — plans
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// src/app/api/billing/plans/route.ts
|
|
45
|
+
import { NextResponse } from 'next/server'
|
|
46
|
+
import { billing } from '@/lib/billing'
|
|
47
|
+
|
|
48
|
+
export async function GET() {
|
|
49
|
+
const plans = await billing.plans.list()
|
|
50
|
+
return NextResponse.json({ plans })
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 5. Route API — checkout Stripe
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// src/app/api/billing/checkout/route.ts
|
|
58
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
59
|
+
import { billing } from '@/lib/billing'
|
|
60
|
+
|
|
61
|
+
export async function POST(req: NextRequest) {
|
|
62
|
+
const { planId, period = 'monthly' } = await req.json()
|
|
63
|
+
const user = await getSession() // votre système d'auth
|
|
64
|
+
|
|
65
|
+
const result = await billing.customers.checkout(user.id, {
|
|
66
|
+
plan_id: planId,
|
|
67
|
+
period,
|
|
68
|
+
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`,
|
|
69
|
+
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return NextResponse.json(result)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 6. Abonnement actif
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/app/api/billing/subscription/route.ts
|
|
80
|
+
import { NextResponse } from 'next/server'
|
|
81
|
+
import { billing } from '@/lib/billing'
|
|
82
|
+
|
|
83
|
+
export async function GET() {
|
|
84
|
+
const user = await getSession()
|
|
85
|
+
|
|
86
|
+
// Synchronise le customer (idempotent)
|
|
87
|
+
await billing.customers.upsert({
|
|
88
|
+
external_id: user.id,
|
|
89
|
+
email: user.email,
|
|
90
|
+
name: user.name,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const { subscription } = await billing.customers.subscription(user.id)
|
|
94
|
+
return NextResponse.json({ subscription })
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
### `BillingClient`
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const billing = new BillingClient({ apiKey: string, baseUrl?: string })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Plans
|
|
107
|
+
|
|
108
|
+
| Méthode | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `billing.plans.list()` | Liste tous les plans disponibles |
|
|
111
|
+
| `billing.plans.get(id)` | Récupère un plan par ID |
|
|
112
|
+
|
|
113
|
+
#### Customers
|
|
114
|
+
|
|
115
|
+
| Méthode | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `billing.customers.upsert(input)` | Crée ou met à jour un customer (idempotent) |
|
|
118
|
+
| `billing.customers.get(externalId)` | Récupère un customer |
|
|
119
|
+
| `billing.customers.subscription(externalId)` | Abonnement actif (null si aucun) |
|
|
120
|
+
| `billing.customers.checkout(externalId, input)` | Crée une Stripe Checkout Session |
|
|
121
|
+
| `billing.customers.portal(externalId, returnUrl)` | URL vers le Stripe Billing Portal |
|
|
122
|
+
| `billing.customers.invoices(externalId)` | Historique des factures |
|
|
123
|
+
|
|
124
|
+
## Types
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
interface SdkPlan {
|
|
128
|
+
id: number
|
|
129
|
+
name: string
|
|
130
|
+
price_monthly: string
|
|
131
|
+
price_yearly: string
|
|
132
|
+
features: string[]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface SdkSubscription {
|
|
136
|
+
id: number
|
|
137
|
+
status: 'active' | 'trialing' | 'past_due' | 'canceled'
|
|
138
|
+
plan: SdkPlan
|
|
139
|
+
current_period_end: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface CheckoutInput {
|
|
143
|
+
plan_id: number
|
|
144
|
+
period?: 'monthly' | 'yearly'
|
|
145
|
+
success_url: string
|
|
146
|
+
cancel_url: string
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
interface UsageEventInput {
|
|
2
|
+
metric: string;
|
|
3
|
+
quantity: number;
|
|
4
|
+
/** Clé unique pour déduplication. Générée automatiquement si absente. */
|
|
5
|
+
idempotencyKey?: string;
|
|
6
|
+
recordedAt?: Date | string;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface UsageEventResponse {
|
|
10
|
+
id: number;
|
|
11
|
+
user_id: number;
|
|
12
|
+
subscription_id: number | null;
|
|
13
|
+
org_id: string | null;
|
|
14
|
+
metric: string;
|
|
15
|
+
quantity: string;
|
|
16
|
+
idempotency_key: string;
|
|
17
|
+
recorded_at: string;
|
|
18
|
+
billed_at: string | null;
|
|
19
|
+
metadata: Record<string, unknown> | null;
|
|
20
|
+
created_at: string;
|
|
21
|
+
updated_at: string;
|
|
22
|
+
}
|
|
23
|
+
interface BatchResult {
|
|
24
|
+
created: number;
|
|
25
|
+
duplicate: number;
|
|
26
|
+
keys: {
|
|
27
|
+
created: string[];
|
|
28
|
+
duplicate: string[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface UsageSummary {
|
|
32
|
+
period_start: string;
|
|
33
|
+
period_end: string;
|
|
34
|
+
usage: Record<string, {
|
|
35
|
+
metric: string;
|
|
36
|
+
total: string;
|
|
37
|
+
events: number;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
interface QuotaMetric {
|
|
41
|
+
used: number;
|
|
42
|
+
limit: number | null;
|
|
43
|
+
pct: number | null;
|
|
44
|
+
status: 'ok' | 'warning' | 'critical' | 'exceeded' | 'unlimited';
|
|
45
|
+
}
|
|
46
|
+
type QuotaStatus = Record<string, QuotaMetric>;
|
|
47
|
+
interface UsageClientOptions {
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
apiToken: string;
|
|
50
|
+
/** Timeout en ms (défaut : 10 000) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare class UsageError extends Error {
|
|
55
|
+
readonly statusCode: number;
|
|
56
|
+
readonly body: unknown;
|
|
57
|
+
constructor(message: string, statusCode: number, body: unknown);
|
|
58
|
+
}
|
|
59
|
+
interface UsageClientAdvancedOptions extends UsageClientOptions {
|
|
60
|
+
/** Active le buffering automatique (défaut: false) */
|
|
61
|
+
autoQueue?: boolean;
|
|
62
|
+
/** Taille max du buffer avant flush forcé (défaut: 50) */
|
|
63
|
+
queueMaxSize?: number;
|
|
64
|
+
/** Intervalle de flush en ms (défaut: 5000) */
|
|
65
|
+
flushInterval?: number;
|
|
66
|
+
/** Nombre de tentatives en cas d'erreur 5xx (défaut: 3) */
|
|
67
|
+
maxRetries?: number;
|
|
68
|
+
/** Délai initial entre retries en ms (défaut: 300) */
|
|
69
|
+
retryDelay?: number;
|
|
70
|
+
/** Callback appelé avant chaque envoi (enrichissement, logging) */
|
|
71
|
+
beforeSend?: (event: UsageEventInput) => UsageEventInput | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Client TypeScript pour l'API Usage de Billing SaaS.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Mode direct
|
|
78
|
+
* const client = new UsageClient({ baseUrl: '...', apiToken: '...' })
|
|
79
|
+
* await client.record({ metric: 'api_calls', quantity: 1 })
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Mode queue (batch automatique)
|
|
83
|
+
* const client = new UsageClient({ baseUrl: '...', apiToken: '...', autoQueue: true })
|
|
84
|
+
* client.track({ metric: 'page_view', quantity: 1 }) // non-bloquant
|
|
85
|
+
*/
|
|
86
|
+
declare class UsageClient {
|
|
87
|
+
private readonly baseUrl;
|
|
88
|
+
private readonly apiToken;
|
|
89
|
+
private readonly timeout;
|
|
90
|
+
private readonly maxRetries;
|
|
91
|
+
private readonly retryDelay;
|
|
92
|
+
private readonly beforeSend?;
|
|
93
|
+
private queue;
|
|
94
|
+
constructor(opts: UsageClientAdvancedOptions);
|
|
95
|
+
/**
|
|
96
|
+
* Enregistre un événement de façon synchrone (direct API call).
|
|
97
|
+
*/
|
|
98
|
+
record(event: UsageEventInput): Promise<UsageEventResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Ajoute un événement dans le buffer (non-bloquant).
|
|
101
|
+
* Nécessite `autoQueue: true` à la construction.
|
|
102
|
+
*/
|
|
103
|
+
track(event: UsageEventInput): void;
|
|
104
|
+
/**
|
|
105
|
+
* Vide le buffer immédiatement (utile avant navigations SPA).
|
|
106
|
+
*/
|
|
107
|
+
flush(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Enregistre jusqu'à 100 événements en une seule requête.
|
|
110
|
+
*/
|
|
111
|
+
batch(events: UsageEventInput[]): Promise<BatchResult>;
|
|
112
|
+
summary(): Promise<UsageSummary>;
|
|
113
|
+
quota(): Promise<QuotaStatus>;
|
|
114
|
+
/** Libère les ressources (timer de la queue). */
|
|
115
|
+
destroy(): void;
|
|
116
|
+
private applyBeforeSend;
|
|
117
|
+
private normalizeEvent;
|
|
118
|
+
private generateKey;
|
|
119
|
+
private post;
|
|
120
|
+
private get;
|
|
121
|
+
private request;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface QueueOptions {
|
|
125
|
+
maxSize?: number;
|
|
126
|
+
flushInterval?: number;
|
|
127
|
+
onFlush: (events: UsageEventInput[]) => Promise<void>;
|
|
128
|
+
onError?: (err: unknown, events: UsageEventInput[]) => void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Buffer d'événements avec flush automatique par intervalle ou par taille.
|
|
132
|
+
* Permet d'envoyer les événements en lot sans bloquer l'appelant.
|
|
133
|
+
*/
|
|
134
|
+
declare class EventQueue {
|
|
135
|
+
private queue;
|
|
136
|
+
private timer;
|
|
137
|
+
private readonly maxSize;
|
|
138
|
+
private readonly onFlush;
|
|
139
|
+
private readonly onError;
|
|
140
|
+
constructor({ maxSize, flushInterval, onFlush, onError }: QueueOptions);
|
|
141
|
+
push(event: UsageEventInput): void;
|
|
142
|
+
flush(): Promise<void>;
|
|
143
|
+
destroy(): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface BillingClientOptions {
|
|
147
|
+
apiKey: string;
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
}
|
|
150
|
+
interface SdkCustomer {
|
|
151
|
+
id: number;
|
|
152
|
+
external_id: string;
|
|
153
|
+
email: string | null;
|
|
154
|
+
name: string | null;
|
|
155
|
+
stripe_customer_id: string | null;
|
|
156
|
+
metadata: Record<string, unknown> | null;
|
|
157
|
+
tenant_id: number;
|
|
158
|
+
active_subscription: SdkSubscription | null;
|
|
159
|
+
created_at: string;
|
|
160
|
+
updated_at: string;
|
|
161
|
+
}
|
|
162
|
+
interface SdkSubscription {
|
|
163
|
+
id: number;
|
|
164
|
+
status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'paused';
|
|
165
|
+
billing_period: 'monthly' | 'yearly';
|
|
166
|
+
current_period_start: string | null;
|
|
167
|
+
current_period_end: string | null;
|
|
168
|
+
trial_ends_at: string | null;
|
|
169
|
+
canceled_at: string | null;
|
|
170
|
+
ends_at: string | null;
|
|
171
|
+
plan: SdkPlan;
|
|
172
|
+
}
|
|
173
|
+
interface SdkPlan {
|
|
174
|
+
id: number;
|
|
175
|
+
name: string;
|
|
176
|
+
slug: string;
|
|
177
|
+
type: string;
|
|
178
|
+
price_monthly: string;
|
|
179
|
+
price_yearly: string;
|
|
180
|
+
currency: string;
|
|
181
|
+
trial_days: number;
|
|
182
|
+
features: Record<string, boolean>;
|
|
183
|
+
max_users: number | null;
|
|
184
|
+
max_api_calls_per_month: number | null;
|
|
185
|
+
max_storage_bytes: number | null;
|
|
186
|
+
}
|
|
187
|
+
interface CheckoutResult {
|
|
188
|
+
checkout_url: string;
|
|
189
|
+
session_id: string;
|
|
190
|
+
}
|
|
191
|
+
interface PortalResult {
|
|
192
|
+
portal_url: string;
|
|
193
|
+
}
|
|
194
|
+
interface UpsertCustomerInput {
|
|
195
|
+
external_id: string;
|
|
196
|
+
email?: string;
|
|
197
|
+
name?: string;
|
|
198
|
+
metadata?: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
interface CheckoutInput {
|
|
201
|
+
plan_id: number;
|
|
202
|
+
period?: 'monthly' | 'yearly';
|
|
203
|
+
success_url: string;
|
|
204
|
+
cancel_url: string;
|
|
205
|
+
coupon?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
declare class BillingError extends Error {
|
|
209
|
+
readonly status: number;
|
|
210
|
+
readonly body: unknown;
|
|
211
|
+
constructor(message: string, status: number, body: unknown);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Client SDK pour intégrer Billing SaaS dans votre application.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* import { BillingClient } from '@billing-saas/js'
|
|
219
|
+
*
|
|
220
|
+
* const billing = new BillingClient({ apiKey: 'bsk_live_...' })
|
|
221
|
+
*
|
|
222
|
+
* // Crée ou retrouve un customer
|
|
223
|
+
* const { customer } = await billing.customers.upsert({
|
|
224
|
+
* external_id: 'user_123',
|
|
225
|
+
* email: 'alice@example.com',
|
|
226
|
+
* })
|
|
227
|
+
*
|
|
228
|
+
* // Lance le checkout Stripe
|
|
229
|
+
* const { checkout_url } = await billing.customers.checkout('user_123', {
|
|
230
|
+
* plan_id: 2,
|
|
231
|
+
* success_url: 'https://yourapp.com/success',
|
|
232
|
+
* cancel_url: 'https://yourapp.com/cancel',
|
|
233
|
+
* })
|
|
234
|
+
*
|
|
235
|
+
* window.location.href = checkout_url
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
declare class BillingClient {
|
|
239
|
+
private readonly baseUrl;
|
|
240
|
+
private readonly headers;
|
|
241
|
+
constructor(options: BillingClientOptions);
|
|
242
|
+
readonly plans: {
|
|
243
|
+
list: () => Promise<SdkPlan[]>;
|
|
244
|
+
get: (id: number) => Promise<SdkPlan>;
|
|
245
|
+
};
|
|
246
|
+
readonly customers: {
|
|
247
|
+
/**
|
|
248
|
+
* Crée ou met à jour un customer (idempotent sur external_id).
|
|
249
|
+
*/
|
|
250
|
+
upsert: (input: UpsertCustomerInput) => Promise<{
|
|
251
|
+
customer: SdkCustomer;
|
|
252
|
+
}>;
|
|
253
|
+
get: (externalId: string) => Promise<{
|
|
254
|
+
customer: SdkCustomer;
|
|
255
|
+
}>;
|
|
256
|
+
delete: (externalId: string) => Promise<{
|
|
257
|
+
message: string;
|
|
258
|
+
}>;
|
|
259
|
+
/**
|
|
260
|
+
* Subscription active du customer (null si aucune).
|
|
261
|
+
*/
|
|
262
|
+
subscription: (externalId: string) => Promise<{
|
|
263
|
+
subscription: SdkSubscription | null;
|
|
264
|
+
}>;
|
|
265
|
+
/**
|
|
266
|
+
* Crée une Stripe Checkout Session — redirigez l'utilisateur vers checkout_url.
|
|
267
|
+
*/
|
|
268
|
+
checkout: (externalId: string, input: CheckoutInput) => Promise<CheckoutResult>;
|
|
269
|
+
/**
|
|
270
|
+
* URL vers le Stripe Billing Portal (gestion CB, annulation, etc.).
|
|
271
|
+
*/
|
|
272
|
+
portal: (externalId: string, returnUrl: string) => Promise<PortalResult>;
|
|
273
|
+
invoices: (externalId: string) => Promise<unknown>;
|
|
274
|
+
};
|
|
275
|
+
private get;
|
|
276
|
+
private post;
|
|
277
|
+
private delete;
|
|
278
|
+
private request;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { type BatchResult, BillingClient, type BillingClientOptions, BillingError, type CheckoutInput, type CheckoutResult, EventQueue, type PortalResult, type QuotaMetric, type QuotaStatus, type SdkCustomer, type SdkPlan, type SdkSubscription, type UpsertCustomerInput, UsageClient, type UsageClientOptions, UsageError, type UsageEventInput, type UsageEventResponse, type UsageSummary };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
interface UsageEventInput {
|
|
2
|
+
metric: string;
|
|
3
|
+
quantity: number;
|
|
4
|
+
/** Clé unique pour déduplication. Générée automatiquement si absente. */
|
|
5
|
+
idempotencyKey?: string;
|
|
6
|
+
recordedAt?: Date | string;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface UsageEventResponse {
|
|
10
|
+
id: number;
|
|
11
|
+
user_id: number;
|
|
12
|
+
subscription_id: number | null;
|
|
13
|
+
org_id: string | null;
|
|
14
|
+
metric: string;
|
|
15
|
+
quantity: string;
|
|
16
|
+
idempotency_key: string;
|
|
17
|
+
recorded_at: string;
|
|
18
|
+
billed_at: string | null;
|
|
19
|
+
metadata: Record<string, unknown> | null;
|
|
20
|
+
created_at: string;
|
|
21
|
+
updated_at: string;
|
|
22
|
+
}
|
|
23
|
+
interface BatchResult {
|
|
24
|
+
created: number;
|
|
25
|
+
duplicate: number;
|
|
26
|
+
keys: {
|
|
27
|
+
created: string[];
|
|
28
|
+
duplicate: string[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface UsageSummary {
|
|
32
|
+
period_start: string;
|
|
33
|
+
period_end: string;
|
|
34
|
+
usage: Record<string, {
|
|
35
|
+
metric: string;
|
|
36
|
+
total: string;
|
|
37
|
+
events: number;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
interface QuotaMetric {
|
|
41
|
+
used: number;
|
|
42
|
+
limit: number | null;
|
|
43
|
+
pct: number | null;
|
|
44
|
+
status: 'ok' | 'warning' | 'critical' | 'exceeded' | 'unlimited';
|
|
45
|
+
}
|
|
46
|
+
type QuotaStatus = Record<string, QuotaMetric>;
|
|
47
|
+
interface UsageClientOptions {
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
apiToken: string;
|
|
50
|
+
/** Timeout en ms (défaut : 10 000) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare class UsageError extends Error {
|
|
55
|
+
readonly statusCode: number;
|
|
56
|
+
readonly body: unknown;
|
|
57
|
+
constructor(message: string, statusCode: number, body: unknown);
|
|
58
|
+
}
|
|
59
|
+
interface UsageClientAdvancedOptions extends UsageClientOptions {
|
|
60
|
+
/** Active le buffering automatique (défaut: false) */
|
|
61
|
+
autoQueue?: boolean;
|
|
62
|
+
/** Taille max du buffer avant flush forcé (défaut: 50) */
|
|
63
|
+
queueMaxSize?: number;
|
|
64
|
+
/** Intervalle de flush en ms (défaut: 5000) */
|
|
65
|
+
flushInterval?: number;
|
|
66
|
+
/** Nombre de tentatives en cas d'erreur 5xx (défaut: 3) */
|
|
67
|
+
maxRetries?: number;
|
|
68
|
+
/** Délai initial entre retries en ms (défaut: 300) */
|
|
69
|
+
retryDelay?: number;
|
|
70
|
+
/** Callback appelé avant chaque envoi (enrichissement, logging) */
|
|
71
|
+
beforeSend?: (event: UsageEventInput) => UsageEventInput | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Client TypeScript pour l'API Usage de Billing SaaS.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Mode direct
|
|
78
|
+
* const client = new UsageClient({ baseUrl: '...', apiToken: '...' })
|
|
79
|
+
* await client.record({ metric: 'api_calls', quantity: 1 })
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Mode queue (batch automatique)
|
|
83
|
+
* const client = new UsageClient({ baseUrl: '...', apiToken: '...', autoQueue: true })
|
|
84
|
+
* client.track({ metric: 'page_view', quantity: 1 }) // non-bloquant
|
|
85
|
+
*/
|
|
86
|
+
declare class UsageClient {
|
|
87
|
+
private readonly baseUrl;
|
|
88
|
+
private readonly apiToken;
|
|
89
|
+
private readonly timeout;
|
|
90
|
+
private readonly maxRetries;
|
|
91
|
+
private readonly retryDelay;
|
|
92
|
+
private readonly beforeSend?;
|
|
93
|
+
private queue;
|
|
94
|
+
constructor(opts: UsageClientAdvancedOptions);
|
|
95
|
+
/**
|
|
96
|
+
* Enregistre un événement de façon synchrone (direct API call).
|
|
97
|
+
*/
|
|
98
|
+
record(event: UsageEventInput): Promise<UsageEventResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Ajoute un événement dans le buffer (non-bloquant).
|
|
101
|
+
* Nécessite `autoQueue: true` à la construction.
|
|
102
|
+
*/
|
|
103
|
+
track(event: UsageEventInput): void;
|
|
104
|
+
/**
|
|
105
|
+
* Vide le buffer immédiatement (utile avant navigations SPA).
|
|
106
|
+
*/
|
|
107
|
+
flush(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Enregistre jusqu'à 100 événements en une seule requête.
|
|
110
|
+
*/
|
|
111
|
+
batch(events: UsageEventInput[]): Promise<BatchResult>;
|
|
112
|
+
summary(): Promise<UsageSummary>;
|
|
113
|
+
quota(): Promise<QuotaStatus>;
|
|
114
|
+
/** Libère les ressources (timer de la queue). */
|
|
115
|
+
destroy(): void;
|
|
116
|
+
private applyBeforeSend;
|
|
117
|
+
private normalizeEvent;
|
|
118
|
+
private generateKey;
|
|
119
|
+
private post;
|
|
120
|
+
private get;
|
|
121
|
+
private request;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface QueueOptions {
|
|
125
|
+
maxSize?: number;
|
|
126
|
+
flushInterval?: number;
|
|
127
|
+
onFlush: (events: UsageEventInput[]) => Promise<void>;
|
|
128
|
+
onError?: (err: unknown, events: UsageEventInput[]) => void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Buffer d'événements avec flush automatique par intervalle ou par taille.
|
|
132
|
+
* Permet d'envoyer les événements en lot sans bloquer l'appelant.
|
|
133
|
+
*/
|
|
134
|
+
declare class EventQueue {
|
|
135
|
+
private queue;
|
|
136
|
+
private timer;
|
|
137
|
+
private readonly maxSize;
|
|
138
|
+
private readonly onFlush;
|
|
139
|
+
private readonly onError;
|
|
140
|
+
constructor({ maxSize, flushInterval, onFlush, onError }: QueueOptions);
|
|
141
|
+
push(event: UsageEventInput): void;
|
|
142
|
+
flush(): Promise<void>;
|
|
143
|
+
destroy(): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface BillingClientOptions {
|
|
147
|
+
apiKey: string;
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
}
|
|
150
|
+
interface SdkCustomer {
|
|
151
|
+
id: number;
|
|
152
|
+
external_id: string;
|
|
153
|
+
email: string | null;
|
|
154
|
+
name: string | null;
|
|
155
|
+
stripe_customer_id: string | null;
|
|
156
|
+
metadata: Record<string, unknown> | null;
|
|
157
|
+
tenant_id: number;
|
|
158
|
+
active_subscription: SdkSubscription | null;
|
|
159
|
+
created_at: string;
|
|
160
|
+
updated_at: string;
|
|
161
|
+
}
|
|
162
|
+
interface SdkSubscription {
|
|
163
|
+
id: number;
|
|
164
|
+
status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'paused';
|
|
165
|
+
billing_period: 'monthly' | 'yearly';
|
|
166
|
+
current_period_start: string | null;
|
|
167
|
+
current_period_end: string | null;
|
|
168
|
+
trial_ends_at: string | null;
|
|
169
|
+
canceled_at: string | null;
|
|
170
|
+
ends_at: string | null;
|
|
171
|
+
plan: SdkPlan;
|
|
172
|
+
}
|
|
173
|
+
interface SdkPlan {
|
|
174
|
+
id: number;
|
|
175
|
+
name: string;
|
|
176
|
+
slug: string;
|
|
177
|
+
type: string;
|
|
178
|
+
price_monthly: string;
|
|
179
|
+
price_yearly: string;
|
|
180
|
+
currency: string;
|
|
181
|
+
trial_days: number;
|
|
182
|
+
features: Record<string, boolean>;
|
|
183
|
+
max_users: number | null;
|
|
184
|
+
max_api_calls_per_month: number | null;
|
|
185
|
+
max_storage_bytes: number | null;
|
|
186
|
+
}
|
|
187
|
+
interface CheckoutResult {
|
|
188
|
+
checkout_url: string;
|
|
189
|
+
session_id: string;
|
|
190
|
+
}
|
|
191
|
+
interface PortalResult {
|
|
192
|
+
portal_url: string;
|
|
193
|
+
}
|
|
194
|
+
interface UpsertCustomerInput {
|
|
195
|
+
external_id: string;
|
|
196
|
+
email?: string;
|
|
197
|
+
name?: string;
|
|
198
|
+
metadata?: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
interface CheckoutInput {
|
|
201
|
+
plan_id: number;
|
|
202
|
+
period?: 'monthly' | 'yearly';
|
|
203
|
+
success_url: string;
|
|
204
|
+
cancel_url: string;
|
|
205
|
+
coupon?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
declare class BillingError extends Error {
|
|
209
|
+
readonly status: number;
|
|
210
|
+
readonly body: unknown;
|
|
211
|
+
constructor(message: string, status: number, body: unknown);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Client SDK pour intégrer Billing SaaS dans votre application.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* import { BillingClient } from '@billing-saas/js'
|
|
219
|
+
*
|
|
220
|
+
* const billing = new BillingClient({ apiKey: 'bsk_live_...' })
|
|
221
|
+
*
|
|
222
|
+
* // Crée ou retrouve un customer
|
|
223
|
+
* const { customer } = await billing.customers.upsert({
|
|
224
|
+
* external_id: 'user_123',
|
|
225
|
+
* email: 'alice@example.com',
|
|
226
|
+
* })
|
|
227
|
+
*
|
|
228
|
+
* // Lance le checkout Stripe
|
|
229
|
+
* const { checkout_url } = await billing.customers.checkout('user_123', {
|
|
230
|
+
* plan_id: 2,
|
|
231
|
+
* success_url: 'https://yourapp.com/success',
|
|
232
|
+
* cancel_url: 'https://yourapp.com/cancel',
|
|
233
|
+
* })
|
|
234
|
+
*
|
|
235
|
+
* window.location.href = checkout_url
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
declare class BillingClient {
|
|
239
|
+
private readonly baseUrl;
|
|
240
|
+
private readonly headers;
|
|
241
|
+
constructor(options: BillingClientOptions);
|
|
242
|
+
readonly plans: {
|
|
243
|
+
list: () => Promise<SdkPlan[]>;
|
|
244
|
+
get: (id: number) => Promise<SdkPlan>;
|
|
245
|
+
};
|
|
246
|
+
readonly customers: {
|
|
247
|
+
/**
|
|
248
|
+
* Crée ou met à jour un customer (idempotent sur external_id).
|
|
249
|
+
*/
|
|
250
|
+
upsert: (input: UpsertCustomerInput) => Promise<{
|
|
251
|
+
customer: SdkCustomer;
|
|
252
|
+
}>;
|
|
253
|
+
get: (externalId: string) => Promise<{
|
|
254
|
+
customer: SdkCustomer;
|
|
255
|
+
}>;
|
|
256
|
+
delete: (externalId: string) => Promise<{
|
|
257
|
+
message: string;
|
|
258
|
+
}>;
|
|
259
|
+
/**
|
|
260
|
+
* Subscription active du customer (null si aucune).
|
|
261
|
+
*/
|
|
262
|
+
subscription: (externalId: string) => Promise<{
|
|
263
|
+
subscription: SdkSubscription | null;
|
|
264
|
+
}>;
|
|
265
|
+
/**
|
|
266
|
+
* Crée une Stripe Checkout Session — redirigez l'utilisateur vers checkout_url.
|
|
267
|
+
*/
|
|
268
|
+
checkout: (externalId: string, input: CheckoutInput) => Promise<CheckoutResult>;
|
|
269
|
+
/**
|
|
270
|
+
* URL vers le Stripe Billing Portal (gestion CB, annulation, etc.).
|
|
271
|
+
*/
|
|
272
|
+
portal: (externalId: string, returnUrl: string) => Promise<PortalResult>;
|
|
273
|
+
invoices: (externalId: string) => Promise<unknown>;
|
|
274
|
+
};
|
|
275
|
+
private get;
|
|
276
|
+
private post;
|
|
277
|
+
private delete;
|
|
278
|
+
private request;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { type BatchResult, BillingClient, type BillingClientOptions, BillingError, type CheckoutInput, type CheckoutResult, EventQueue, type PortalResult, type QuotaMetric, type QuotaStatus, type SdkCustomer, type SdkPlan, type SdkSubscription, type UpsertCustomerInput, UsageClient, type UsageClientOptions, UsageError, type UsageEventInput, type UsageEventResponse, type UsageSummary };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/index.ts
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
BillingClient: () => BillingClient,
|
|
23
|
+
BillingError: () => BillingError,
|
|
24
|
+
EventQueue: () => EventQueue,
|
|
25
|
+
UsageClient: () => UsageClient,
|
|
26
|
+
UsageError: () => UsageError
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/EventQueue.ts
|
|
31
|
+
var EventQueue = class {
|
|
32
|
+
queue = [];
|
|
33
|
+
timer = null;
|
|
34
|
+
maxSize;
|
|
35
|
+
onFlush;
|
|
36
|
+
onError;
|
|
37
|
+
constructor({ maxSize = 50, flushInterval = 5e3, onFlush, onError }) {
|
|
38
|
+
this.maxSize = maxSize;
|
|
39
|
+
this.onFlush = onFlush;
|
|
40
|
+
this.onError = onError ?? ((err) => console.error("[UsageQueue]", err));
|
|
41
|
+
if (flushInterval > 0 && typeof setInterval !== "undefined") {
|
|
42
|
+
this.timer = setInterval(() => void this.flush(), flushInterval);
|
|
43
|
+
}
|
|
44
|
+
if (typeof window !== "undefined") {
|
|
45
|
+
window.addEventListener("visibilitychange", () => {
|
|
46
|
+
if (document.visibilityState === "hidden") void this.flush();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
push(event) {
|
|
51
|
+
this.queue.push(event);
|
|
52
|
+
if (this.queue.length >= this.maxSize) {
|
|
53
|
+
void this.flush();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async flush() {
|
|
57
|
+
if (this.queue.length === 0) return;
|
|
58
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
59
|
+
try {
|
|
60
|
+
await this.onFlush(batch);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
this.onError(err, batch);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
destroy() {
|
|
66
|
+
if (this.timer !== null) {
|
|
67
|
+
clearInterval(this.timer);
|
|
68
|
+
this.timer = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/UsageClient.ts
|
|
74
|
+
var UsageError = class extends Error {
|
|
75
|
+
constructor(message, statusCode, body) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.statusCode = statusCode;
|
|
78
|
+
this.body = body;
|
|
79
|
+
this.name = "UsageError";
|
|
80
|
+
}
|
|
81
|
+
statusCode;
|
|
82
|
+
body;
|
|
83
|
+
};
|
|
84
|
+
var UsageClient = class {
|
|
85
|
+
baseUrl;
|
|
86
|
+
apiToken;
|
|
87
|
+
timeout;
|
|
88
|
+
maxRetries;
|
|
89
|
+
retryDelay;
|
|
90
|
+
beforeSend;
|
|
91
|
+
queue = null;
|
|
92
|
+
constructor(opts) {
|
|
93
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
94
|
+
this.apiToken = opts.apiToken;
|
|
95
|
+
this.timeout = opts.timeout ?? 1e4;
|
|
96
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
97
|
+
this.retryDelay = opts.retryDelay ?? 300;
|
|
98
|
+
this.beforeSend = opts.beforeSend;
|
|
99
|
+
if (opts.autoQueue) {
|
|
100
|
+
this.queue = new EventQueue({
|
|
101
|
+
maxSize: opts.queueMaxSize ?? 50,
|
|
102
|
+
flushInterval: opts.flushInterval ?? 5e3,
|
|
103
|
+
onFlush: (events) => this.batch(events).then(() => void 0),
|
|
104
|
+
onError: (err) => console.error("[UsageClient:queue]", err)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Enregistre un événement de façon synchrone (direct API call).
|
|
110
|
+
*/
|
|
111
|
+
async record(event) {
|
|
112
|
+
const payload = this.applyBeforeSend(this.normalizeEvent(event));
|
|
113
|
+
if (!payload) return Promise.reject(new Error("beforeSend filtered event"));
|
|
114
|
+
return this.post("/v1/usage/events", payload);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Ajoute un événement dans le buffer (non-bloquant).
|
|
118
|
+
* Nécessite `autoQueue: true` à la construction.
|
|
119
|
+
*/
|
|
120
|
+
track(event) {
|
|
121
|
+
if (!this.queue) {
|
|
122
|
+
throw new Error("autoQueue est d\xE9sactiv\xE9. Passez autoQueue:true ou utilisez record().");
|
|
123
|
+
}
|
|
124
|
+
const normalized = this.applyBeforeSend(this.normalizeEvent(event));
|
|
125
|
+
if (normalized) this.queue.push(normalized);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Vide le buffer immédiatement (utile avant navigations SPA).
|
|
129
|
+
*/
|
|
130
|
+
async flush() {
|
|
131
|
+
var _a;
|
|
132
|
+
await ((_a = this.queue) == null ? void 0 : _a.flush());
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Enregistre jusqu'à 100 événements en une seule requête.
|
|
136
|
+
*/
|
|
137
|
+
async batch(events) {
|
|
138
|
+
if (events.length === 0) {
|
|
139
|
+
return { created: 0, duplicate: 0, keys: { created: [], duplicate: [] } };
|
|
140
|
+
}
|
|
141
|
+
if (events.length > 100) {
|
|
142
|
+
throw new RangeError("Le lot ne peut pas d\xE9passer 100 \xE9v\xE9nements.");
|
|
143
|
+
}
|
|
144
|
+
return this.post("/v1/usage/batch", {
|
|
145
|
+
events: events.map((e) => this.normalizeEvent(e))
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async summary() {
|
|
149
|
+
return this.get("/v1/usage/summary");
|
|
150
|
+
}
|
|
151
|
+
async quota() {
|
|
152
|
+
return this.get("/v1/usage/quota");
|
|
153
|
+
}
|
|
154
|
+
/** Libère les ressources (timer de la queue). */
|
|
155
|
+
destroy() {
|
|
156
|
+
var _a;
|
|
157
|
+
(_a = this.queue) == null ? void 0 : _a.destroy();
|
|
158
|
+
}
|
|
159
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
160
|
+
applyBeforeSend(event) {
|
|
161
|
+
return this.beforeSend ? this.beforeSend(event) : event;
|
|
162
|
+
}
|
|
163
|
+
normalizeEvent(event) {
|
|
164
|
+
return {
|
|
165
|
+
...event,
|
|
166
|
+
idempotencyKey: event.idempotencyKey ?? this.generateKey(event.metric)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
generateKey(metric) {
|
|
170
|
+
const rand = Math.random().toString(36).slice(2);
|
|
171
|
+
return `${metric}:${Date.now()}:${rand}`;
|
|
172
|
+
}
|
|
173
|
+
async post(path, body) {
|
|
174
|
+
return this.request("POST", path, body);
|
|
175
|
+
}
|
|
176
|
+
async get(path, params) {
|
|
177
|
+
const url = params ? `${path}?${new URLSearchParams(params)}` : path;
|
|
178
|
+
return this.request("GET", url);
|
|
179
|
+
}
|
|
180
|
+
async request(method, path, body) {
|
|
181
|
+
let lastError;
|
|
182
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
183
|
+
try {
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
186
|
+
try {
|
|
187
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
188
|
+
method,
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
Accept: "application/json"
|
|
193
|
+
},
|
|
194
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
195
|
+
signal: controller.signal
|
|
196
|
+
});
|
|
197
|
+
const data = await resp.json().catch(() => ({}));
|
|
198
|
+
if (!resp.ok) {
|
|
199
|
+
const message = data["message"] ?? `HTTP ${resp.status}`;
|
|
200
|
+
const err = new UsageError(message, resp.status, data);
|
|
201
|
+
if (resp.status >= 400 && resp.status < 500) throw err;
|
|
202
|
+
lastError = err;
|
|
203
|
+
} else {
|
|
204
|
+
return data;
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err instanceof UsageError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
lastError = err;
|
|
214
|
+
}
|
|
215
|
+
if (attempt < this.maxRetries - 1) {
|
|
216
|
+
await sleep(this.retryDelay * 2 ** attempt);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw lastError;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
function sleep(ms) {
|
|
223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/BillingClient.ts
|
|
227
|
+
var BillingError = class extends Error {
|
|
228
|
+
constructor(message, status, body) {
|
|
229
|
+
super(message);
|
|
230
|
+
this.status = status;
|
|
231
|
+
this.body = body;
|
|
232
|
+
this.name = "BillingError";
|
|
233
|
+
}
|
|
234
|
+
status;
|
|
235
|
+
body;
|
|
236
|
+
};
|
|
237
|
+
var BillingClient = class {
|
|
238
|
+
baseUrl;
|
|
239
|
+
headers;
|
|
240
|
+
constructor(options) {
|
|
241
|
+
this.baseUrl = (options.baseUrl ?? "http://localhost:8000/api").replace(/\/$/, "");
|
|
242
|
+
this.headers = {
|
|
243
|
+
"Content-Type": "application/json",
|
|
244
|
+
"Authorization": `Bearer ${options.apiKey}`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// ─── Plans ────────────────────────────────────────────────────────────────
|
|
248
|
+
plans = {
|
|
249
|
+
list: () => this.get("/v1/sdk/plans"),
|
|
250
|
+
get: (id) => this.get(`/v1/sdk/plans/${id}`)
|
|
251
|
+
};
|
|
252
|
+
// ─── Customers ────────────────────────────────────────────────────────────
|
|
253
|
+
customers = {
|
|
254
|
+
/**
|
|
255
|
+
* Crée ou met à jour un customer (idempotent sur external_id).
|
|
256
|
+
*/
|
|
257
|
+
upsert: (input) => this.post("/v1/sdk/customers", input),
|
|
258
|
+
get: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}`),
|
|
259
|
+
delete: (externalId) => this.delete(`/v1/sdk/customers/${encodeURIComponent(externalId)}`),
|
|
260
|
+
/**
|
|
261
|
+
* Subscription active du customer (null si aucune).
|
|
262
|
+
*/
|
|
263
|
+
subscription: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}/subscription`),
|
|
264
|
+
/**
|
|
265
|
+
* Crée une Stripe Checkout Session — redirigez l'utilisateur vers checkout_url.
|
|
266
|
+
*/
|
|
267
|
+
checkout: (externalId, input) => this.post(`/v1/sdk/customers/${encodeURIComponent(externalId)}/checkout`, input),
|
|
268
|
+
/**
|
|
269
|
+
* URL vers le Stripe Billing Portal (gestion CB, annulation, etc.).
|
|
270
|
+
*/
|
|
271
|
+
portal: (externalId, returnUrl) => this.post(`/v1/sdk/customers/${encodeURIComponent(externalId)}/portal`, { return_url: returnUrl }),
|
|
272
|
+
invoices: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}/invoices`)
|
|
273
|
+
};
|
|
274
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────
|
|
275
|
+
async get(path) {
|
|
276
|
+
return this.request("GET", path);
|
|
277
|
+
}
|
|
278
|
+
async post(path, body) {
|
|
279
|
+
return this.request("POST", path, body);
|
|
280
|
+
}
|
|
281
|
+
async delete(path) {
|
|
282
|
+
return this.request("DELETE", path);
|
|
283
|
+
}
|
|
284
|
+
async request(method, path, body) {
|
|
285
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
286
|
+
method,
|
|
287
|
+
headers: this.headers,
|
|
288
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
289
|
+
});
|
|
290
|
+
const data = await res.json().catch(() => null);
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
throw new BillingError(
|
|
293
|
+
(data == null ? void 0 : data.message) ?? `HTTP ${res.status}`,
|
|
294
|
+
res.status,
|
|
295
|
+
data
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return data;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
302
|
+
0 && (module.exports = {
|
|
303
|
+
BillingClient,
|
|
304
|
+
BillingError,
|
|
305
|
+
EventQueue,
|
|
306
|
+
UsageClient,
|
|
307
|
+
UsageError
|
|
308
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// src/EventQueue.ts
|
|
2
|
+
var EventQueue = class {
|
|
3
|
+
queue = [];
|
|
4
|
+
timer = null;
|
|
5
|
+
maxSize;
|
|
6
|
+
onFlush;
|
|
7
|
+
onError;
|
|
8
|
+
constructor({ maxSize = 50, flushInterval = 5e3, onFlush, onError }) {
|
|
9
|
+
this.maxSize = maxSize;
|
|
10
|
+
this.onFlush = onFlush;
|
|
11
|
+
this.onError = onError ?? ((err) => console.error("[UsageQueue]", err));
|
|
12
|
+
if (flushInterval > 0 && typeof setInterval !== "undefined") {
|
|
13
|
+
this.timer = setInterval(() => void this.flush(), flushInterval);
|
|
14
|
+
}
|
|
15
|
+
if (typeof window !== "undefined") {
|
|
16
|
+
window.addEventListener("visibilitychange", () => {
|
|
17
|
+
if (document.visibilityState === "hidden") void this.flush();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
push(event) {
|
|
22
|
+
this.queue.push(event);
|
|
23
|
+
if (this.queue.length >= this.maxSize) {
|
|
24
|
+
void this.flush();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async flush() {
|
|
28
|
+
if (this.queue.length === 0) return;
|
|
29
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
30
|
+
try {
|
|
31
|
+
await this.onFlush(batch);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
this.onError(err, batch);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
destroy() {
|
|
37
|
+
if (this.timer !== null) {
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/UsageClient.ts
|
|
45
|
+
var UsageError = class extends Error {
|
|
46
|
+
constructor(message, statusCode, body) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.statusCode = statusCode;
|
|
49
|
+
this.body = body;
|
|
50
|
+
this.name = "UsageError";
|
|
51
|
+
}
|
|
52
|
+
statusCode;
|
|
53
|
+
body;
|
|
54
|
+
};
|
|
55
|
+
var UsageClient = class {
|
|
56
|
+
baseUrl;
|
|
57
|
+
apiToken;
|
|
58
|
+
timeout;
|
|
59
|
+
maxRetries;
|
|
60
|
+
retryDelay;
|
|
61
|
+
beforeSend;
|
|
62
|
+
queue = null;
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
65
|
+
this.apiToken = opts.apiToken;
|
|
66
|
+
this.timeout = opts.timeout ?? 1e4;
|
|
67
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
68
|
+
this.retryDelay = opts.retryDelay ?? 300;
|
|
69
|
+
this.beforeSend = opts.beforeSend;
|
|
70
|
+
if (opts.autoQueue) {
|
|
71
|
+
this.queue = new EventQueue({
|
|
72
|
+
maxSize: opts.queueMaxSize ?? 50,
|
|
73
|
+
flushInterval: opts.flushInterval ?? 5e3,
|
|
74
|
+
onFlush: (events) => this.batch(events).then(() => void 0),
|
|
75
|
+
onError: (err) => console.error("[UsageClient:queue]", err)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Enregistre un événement de façon synchrone (direct API call).
|
|
81
|
+
*/
|
|
82
|
+
async record(event) {
|
|
83
|
+
const payload = this.applyBeforeSend(this.normalizeEvent(event));
|
|
84
|
+
if (!payload) return Promise.reject(new Error("beforeSend filtered event"));
|
|
85
|
+
return this.post("/v1/usage/events", payload);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Ajoute un événement dans le buffer (non-bloquant).
|
|
89
|
+
* Nécessite `autoQueue: true` à la construction.
|
|
90
|
+
*/
|
|
91
|
+
track(event) {
|
|
92
|
+
if (!this.queue) {
|
|
93
|
+
throw new Error("autoQueue est d\xE9sactiv\xE9. Passez autoQueue:true ou utilisez record().");
|
|
94
|
+
}
|
|
95
|
+
const normalized = this.applyBeforeSend(this.normalizeEvent(event));
|
|
96
|
+
if (normalized) this.queue.push(normalized);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Vide le buffer immédiatement (utile avant navigations SPA).
|
|
100
|
+
*/
|
|
101
|
+
async flush() {
|
|
102
|
+
var _a;
|
|
103
|
+
await ((_a = this.queue) == null ? void 0 : _a.flush());
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Enregistre jusqu'à 100 événements en une seule requête.
|
|
107
|
+
*/
|
|
108
|
+
async batch(events) {
|
|
109
|
+
if (events.length === 0) {
|
|
110
|
+
return { created: 0, duplicate: 0, keys: { created: [], duplicate: [] } };
|
|
111
|
+
}
|
|
112
|
+
if (events.length > 100) {
|
|
113
|
+
throw new RangeError("Le lot ne peut pas d\xE9passer 100 \xE9v\xE9nements.");
|
|
114
|
+
}
|
|
115
|
+
return this.post("/v1/usage/batch", {
|
|
116
|
+
events: events.map((e) => this.normalizeEvent(e))
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async summary() {
|
|
120
|
+
return this.get("/v1/usage/summary");
|
|
121
|
+
}
|
|
122
|
+
async quota() {
|
|
123
|
+
return this.get("/v1/usage/quota");
|
|
124
|
+
}
|
|
125
|
+
/** Libère les ressources (timer de la queue). */
|
|
126
|
+
destroy() {
|
|
127
|
+
var _a;
|
|
128
|
+
(_a = this.queue) == null ? void 0 : _a.destroy();
|
|
129
|
+
}
|
|
130
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
131
|
+
applyBeforeSend(event) {
|
|
132
|
+
return this.beforeSend ? this.beforeSend(event) : event;
|
|
133
|
+
}
|
|
134
|
+
normalizeEvent(event) {
|
|
135
|
+
return {
|
|
136
|
+
...event,
|
|
137
|
+
idempotencyKey: event.idempotencyKey ?? this.generateKey(event.metric)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
generateKey(metric) {
|
|
141
|
+
const rand = Math.random().toString(36).slice(2);
|
|
142
|
+
return `${metric}:${Date.now()}:${rand}`;
|
|
143
|
+
}
|
|
144
|
+
async post(path, body) {
|
|
145
|
+
return this.request("POST", path, body);
|
|
146
|
+
}
|
|
147
|
+
async get(path, params) {
|
|
148
|
+
const url = params ? `${path}?${new URLSearchParams(params)}` : path;
|
|
149
|
+
return this.request("GET", url);
|
|
150
|
+
}
|
|
151
|
+
async request(method, path, body) {
|
|
152
|
+
let lastError;
|
|
153
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
157
|
+
try {
|
|
158
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
Accept: "application/json"
|
|
164
|
+
},
|
|
165
|
+
body: body != null ? JSON.stringify(body) : void 0,
|
|
166
|
+
signal: controller.signal
|
|
167
|
+
});
|
|
168
|
+
const data = await resp.json().catch(() => ({}));
|
|
169
|
+
if (!resp.ok) {
|
|
170
|
+
const message = data["message"] ?? `HTTP ${resp.status}`;
|
|
171
|
+
const err = new UsageError(message, resp.status, data);
|
|
172
|
+
if (resp.status >= 400 && resp.status < 500) throw err;
|
|
173
|
+
lastError = err;
|
|
174
|
+
} else {
|
|
175
|
+
return data;
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err instanceof UsageError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
lastError = err;
|
|
185
|
+
}
|
|
186
|
+
if (attempt < this.maxRetries - 1) {
|
|
187
|
+
await sleep(this.retryDelay * 2 ** attempt);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw lastError;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function sleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/BillingClient.ts
|
|
198
|
+
var BillingError = class extends Error {
|
|
199
|
+
constructor(message, status, body) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.status = status;
|
|
202
|
+
this.body = body;
|
|
203
|
+
this.name = "BillingError";
|
|
204
|
+
}
|
|
205
|
+
status;
|
|
206
|
+
body;
|
|
207
|
+
};
|
|
208
|
+
var BillingClient = class {
|
|
209
|
+
baseUrl;
|
|
210
|
+
headers;
|
|
211
|
+
constructor(options) {
|
|
212
|
+
this.baseUrl = (options.baseUrl ?? "http://localhost:8000/api").replace(/\/$/, "");
|
|
213
|
+
this.headers = {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
"Authorization": `Bearer ${options.apiKey}`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ─── Plans ────────────────────────────────────────────────────────────────
|
|
219
|
+
plans = {
|
|
220
|
+
list: () => this.get("/v1/sdk/plans"),
|
|
221
|
+
get: (id) => this.get(`/v1/sdk/plans/${id}`)
|
|
222
|
+
};
|
|
223
|
+
// ─── Customers ────────────────────────────────────────────────────────────
|
|
224
|
+
customers = {
|
|
225
|
+
/**
|
|
226
|
+
* Crée ou met à jour un customer (idempotent sur external_id).
|
|
227
|
+
*/
|
|
228
|
+
upsert: (input) => this.post("/v1/sdk/customers", input),
|
|
229
|
+
get: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}`),
|
|
230
|
+
delete: (externalId) => this.delete(`/v1/sdk/customers/${encodeURIComponent(externalId)}`),
|
|
231
|
+
/**
|
|
232
|
+
* Subscription active du customer (null si aucune).
|
|
233
|
+
*/
|
|
234
|
+
subscription: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}/subscription`),
|
|
235
|
+
/**
|
|
236
|
+
* Crée une Stripe Checkout Session — redirigez l'utilisateur vers checkout_url.
|
|
237
|
+
*/
|
|
238
|
+
checkout: (externalId, input) => this.post(`/v1/sdk/customers/${encodeURIComponent(externalId)}/checkout`, input),
|
|
239
|
+
/**
|
|
240
|
+
* URL vers le Stripe Billing Portal (gestion CB, annulation, etc.).
|
|
241
|
+
*/
|
|
242
|
+
portal: (externalId, returnUrl) => this.post(`/v1/sdk/customers/${encodeURIComponent(externalId)}/portal`, { return_url: returnUrl }),
|
|
243
|
+
invoices: (externalId) => this.get(`/v1/sdk/customers/${encodeURIComponent(externalId)}/invoices`)
|
|
244
|
+
};
|
|
245
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────
|
|
246
|
+
async get(path) {
|
|
247
|
+
return this.request("GET", path);
|
|
248
|
+
}
|
|
249
|
+
async post(path, body) {
|
|
250
|
+
return this.request("POST", path, body);
|
|
251
|
+
}
|
|
252
|
+
async delete(path) {
|
|
253
|
+
return this.request("DELETE", path);
|
|
254
|
+
}
|
|
255
|
+
async request(method, path, body) {
|
|
256
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
257
|
+
method,
|
|
258
|
+
headers: this.headers,
|
|
259
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
260
|
+
});
|
|
261
|
+
const data = await res.json().catch(() => null);
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
throw new BillingError(
|
|
264
|
+
(data == null ? void 0 : data.message) ?? `HTTP ${res.status}`,
|
|
265
|
+
res.status,
|
|
266
|
+
data
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return data;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
export {
|
|
273
|
+
BillingClient,
|
|
274
|
+
BillingError,
|
|
275
|
+
EventQueue,
|
|
276
|
+
UsageClient,
|
|
277
|
+
UsageError
|
|
278
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@billing-saas/usage-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK TypeScript pour intégrer le PU Billing SaaS dans votre application Next.js — gestion des plans, checkout Stripe, abonnements et feature gates.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"billing",
|
|
26
|
+
"saas",
|
|
27
|
+
"subscription",
|
|
28
|
+
"stripe",
|
|
29
|
+
"payments",
|
|
30
|
+
"nextjs",
|
|
31
|
+
"sdk"
|
|
32
|
+
],
|
|
33
|
+
"author": "Billing SaaS",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.0.0",
|
|
38
|
+
"vitest": "^1.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|