@voltro/plugin-billing 0.85.0 → 0.87.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 +5987 -0
- package/README.md +435 -0
- package/THIRD-PARTY-NOTICES.md +864 -253
- package/dist/catalog.js +471 -0
- package/dist/checkoutStore.js +165 -0
- package/dist/closeStore.js +125 -0
- package/dist/custodyInventory.js +155 -0
- package/dist/customerStore.js +259 -0
- package/dist/dunning.js +209 -0
- package/dist/economy.d.ts +30 -0
- package/dist/economy.js +266 -0
- package/dist/entitlement.js +41 -0
- package/dist/eventDelivery.js +72 -0
- package/dist/flushClaim.js +20 -0
- package/dist/index.d.ts +669 -65
- package/dist/index.js +243 -2803
- package/dist/invoiceHistory.js +91 -0
- package/dist/invoiceHistoryJournal.js +476 -0
- package/dist/invoiceHistoryProgress.js +235 -0
- package/dist/invoiceHistoryWork.js +363 -0
- package/dist/invoiceSnapshot.js +66 -0
- package/dist/invoiceStore.js +133 -0
- package/dist/metering.js +74 -0
- package/dist/plans.js +24 -0
- package/dist/providerIdentity.js +24 -0
- package/dist/providers.js +338 -0
- package/dist/provision.js +153 -0
- package/dist/service.js +1116 -0
- package/dist/stores.js +260 -0
- package/dist/stripeClient.js +53 -0
- package/dist/stripeIdentity.js +56 -0
- package/dist/stripeInvoiceHistory.js +67 -0
- package/dist/stripeInvoiceSnapshot.js +86 -0
- package/dist/stripeProvider.js +479 -0
- package/dist/stripeRest.js +52 -0
- package/dist/stripeVerifiedEvent.js +29 -0
- package/dist/table.js +77 -0
- package/dist/types.d.ts +298 -27
- package/dist/usageEvents.js +65 -0
- package/dist/webhook.js +38 -0
- package/package.json +17 -7
package/dist/types.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ declare const BillingError_base: Schema.Class<BillingError, Schema.TaggedStruct<
|
|
|
30
30
|
}>, YieldableError>;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* The
|
|
33
|
+
* The tenant-resolved event union dispatched after `BillingService.applyEvent` accepts an event
|
|
34
34
|
* from a verified, decoded provider webhook payload. `BillingService`
|
|
35
35
|
* applies these to the DB rows — switching provider swaps the adapter;
|
|
36
36
|
* the service + rows + entitlement engine are unchanged.
|
|
@@ -86,17 +86,29 @@ export declare type BillingEvent = {
|
|
|
86
86
|
/** The event tag union — handy for `onEvent` keys. */
|
|
87
87
|
export declare type BillingEventTag = BillingEvent['_tag'];
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
export declare type BillingInvoiceTrigger = BillingInvoiceTriggerFields & ({
|
|
90
|
+
readonly _tag: 'invoicePaid';
|
|
91
|
+
} | {
|
|
92
|
+
readonly _tag: 'invoicePaymentFailed';
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** Verified provider invoice trigger. Tenant metadata is never ownership evidence. */
|
|
96
|
+
declare interface BillingInvoiceTriggerFields {
|
|
97
|
+
readonly providerCustomerId: string;
|
|
98
|
+
readonly providerInvoiceId: string;
|
|
99
|
+
readonly amountMinor: number;
|
|
100
|
+
readonly currency: string;
|
|
101
|
+
readonly occurredAt: Date | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
export declare interface BillingProvider {
|
|
105
|
+
/** Required by account-qualified custody consumers; absence is unsupported, not fallback authority. */
|
|
106
|
+
readonly fetchAccountIdentity?: () => Effect.Effect<ProviderAccountIdentity, BillingError>;
|
|
107
|
+
/** Must reject a different account/mode before reading customer data. A 404 is not positive deletion evidence. */
|
|
108
|
+
readonly fetchCustomerSnapshot?: (input: {
|
|
109
|
+
readonly account: ProviderAccountIdentity;
|
|
110
|
+
readonly providerCustomerId: string;
|
|
111
|
+
}) => Effect.Effect<ProviderCustomerSnapshot, BillingError>;
|
|
100
112
|
readonly name: string;
|
|
101
113
|
/** Whether the provider supports server-pushed metered usage. Stripe
|
|
102
114
|
* does (subscription-item usage records); a flat-plan provider may not.
|
|
@@ -107,9 +119,13 @@ export declare interface BillingProvider {
|
|
|
107
119
|
* meters map events to a customer by id, so an unlinked tenant's usage
|
|
108
120
|
* stays pending instead of being sent unattributable. */
|
|
109
121
|
readonly usageRequiresCustomer?: boolean;
|
|
110
|
-
readonly createCheckoutSession: (input: CheckoutInput) => Effect.Effect<
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
readonly createCheckoutSession: (input: CheckoutInput) => Effect.Effect<CheckoutSessionReceipt, BillingError>;
|
|
123
|
+
/** Read positive evidence for one exact persisted checkout intent. */
|
|
124
|
+
readonly findCheckoutSession: (input: {
|
|
125
|
+
readonly tenantId: string;
|
|
126
|
+
readonly providerCustomerId?: string;
|
|
127
|
+
readonly operationId: string;
|
|
128
|
+
}) => Effect.Effect<CheckoutSessionLookup, BillingError>;
|
|
113
129
|
readonly createPortalSession: (input: PortalInput) => Effect.Effect<{
|
|
114
130
|
url: string;
|
|
115
131
|
}, BillingError>;
|
|
@@ -141,6 +157,10 @@ export declare interface BillingProvider {
|
|
|
141
157
|
* matters — the subscription's status right now does. Returns null when the
|
|
142
158
|
* provider does not know the id.
|
|
143
159
|
*/
|
|
160
|
+
/** Direct customer-qualified observation, including terminal subscriptions.
|
|
161
|
+
* Null is absence, never proof of cancellation. Required for retired-customer
|
|
162
|
+
* reconciliation and for subscriptions absent from the live listing. */
|
|
163
|
+
readonly fetchSubscriptionSnapshot?: (providerSubscriptionId: string) => Effect.Effect<ProviderSubscriptionSnapshot | null, BillingError>;
|
|
144
164
|
readonly fetchSubscription: (providerSubscriptionId: string) => Effect.Effect<ProviderSubscriptionState | null, BillingError>;
|
|
145
165
|
/**
|
|
146
166
|
* Every live (not yet ended) subscription at the provider, optionally for
|
|
@@ -150,12 +170,19 @@ export declare interface BillingProvider {
|
|
|
150
170
|
readonly listSubscriptions: (input: {
|
|
151
171
|
readonly providerCustomerId?: string;
|
|
152
172
|
}) => Effect.Effect<ReadonlyArray<ProviderSubscriptionSnapshot>, BillingError>;
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
173
|
+
/** Quote the provider-clock settlement deadline after final usage dispatch.
|
|
174
|
+
* Persist it before requesting account-close cancellation. */
|
|
175
|
+
readonly prepareSubscriptionClose: (providerSubscriptionId: string) => Effect.Effect<{
|
|
176
|
+
readonly cancelAt: Date;
|
|
177
|
+
}, BillingError>;
|
|
155
178
|
readonly cancelSubscription: (input: {
|
|
156
179
|
readonly providerSubscriptionId: string;
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
} & ({
|
|
181
|
+
readonly mode: 'periodEnd';
|
|
182
|
+
} | {
|
|
183
|
+
readonly mode: 'close';
|
|
184
|
+
readonly closeOperation: SubscriptionCloseOperation;
|
|
185
|
+
})) => Effect.Effect<ProviderSubscriptionState, BillingError>;
|
|
159
186
|
/** Undo a period-end cancellation that has not taken effect yet. */
|
|
160
187
|
readonly resumeSubscription: (providerSubscriptionId: string) => Effect.Effect<ProviderSubscriptionState, BillingError>;
|
|
161
188
|
/**
|
|
@@ -172,6 +199,7 @@ export declare interface BillingProvider {
|
|
|
172
199
|
* can be paid. Returns how many were expired. */
|
|
173
200
|
readonly expireOpenCheckouts: (input: {
|
|
174
201
|
readonly providerCustomerId: string;
|
|
202
|
+
readonly exceptOperationId?: string;
|
|
175
203
|
}) => Effect.Effect<number, BillingError>;
|
|
176
204
|
/** Add prices the subscription is missing (metered items), without
|
|
177
205
|
* proration. Prices already present are left alone. */
|
|
@@ -185,12 +213,22 @@ export declare interface BillingProvider {
|
|
|
185
213
|
* the app keeps its own. Optional: a provider may model no customer email.
|
|
186
214
|
*/
|
|
187
215
|
readonly customerEmail?: (providerCustomerId: string) => Effect.Effect<string | null, BillingError>;
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
216
|
+
/** Bounded recent invoice preview, not complete history. Provider-hosted
|
|
217
|
+
* links remain theirs; use listInvoiceHistory for explicit cursor traversal. */
|
|
190
218
|
readonly listInvoices: (input: {
|
|
191
219
|
readonly providerCustomerId: string;
|
|
192
220
|
readonly limit?: number;
|
|
193
221
|
}) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
|
|
222
|
+
/** Explicit bounded history pages, including draft and terminal statuses.
|
|
223
|
+
* Cursor exhaustion describes this traversal, not a stable global snapshot.
|
|
224
|
+
* Unsupported adapters must fail; the latest-preview list is no fallback. */
|
|
225
|
+
readonly listInvoiceHistory?: (input: ProviderInvoiceHistoryInput) => Effect.Effect<ProviderInvoiceHistoryPage, BillingError>;
|
|
226
|
+
/** Read one finalized invoice with every line, after confirming its customer.
|
|
227
|
+
* Absence means this provider cannot supply detailed invoice evidence. */
|
|
228
|
+
readonly fetchInvoiceSnapshot?: (input: {
|
|
229
|
+
readonly providerCustomerId: string;
|
|
230
|
+
readonly providerInvoiceId: string;
|
|
231
|
+
}) => Effect.Effect<ProviderInvoiceSnapshot, BillingError>;
|
|
194
232
|
/**
|
|
195
233
|
* Verify a webhook signature and decode the payload, using the provider's
|
|
196
234
|
* OWN verification. Returns `null` when this provider does not do its own
|
|
@@ -201,10 +239,10 @@ export declare interface BillingProvider {
|
|
|
201
239
|
readonly signatureHeader: string;
|
|
202
240
|
readonly secret: string;
|
|
203
241
|
}) => Effect.Effect<unknown, BillingError>;
|
|
204
|
-
/** Map a verified, decoded webhook payload to a `
|
|
242
|
+
/** Map a verified, decoded webhook payload to a `BillingProviderEvent`, or
|
|
205
243
|
* `null` for an event type this provider doesn't model (handler
|
|
206
244
|
* no-ops, still 200). */
|
|
207
|
-
readonly normalizeEvent: (raw: unknown) => Effect.Effect<
|
|
245
|
+
readonly normalizeEvent: (raw: unknown) => Effect.Effect<BillingProviderEvent | null, BillingError>;
|
|
208
246
|
/**
|
|
209
247
|
* Who and when a verified payload is about: the provider customer it names
|
|
210
248
|
* and when the provider created it. The webhook syncs that customer from a
|
|
@@ -221,9 +259,17 @@ export declare interface BillingProvider {
|
|
|
221
259
|
* it keeps them (Stripe: 30 days) — what an outage of the endpoint left
|
|
222
260
|
* behind. Optional: a provider without it has no catch-up.
|
|
223
261
|
*/
|
|
224
|
-
readonly listUndeliveredEvents?: (
|
|
262
|
+
readonly listUndeliveredEvents?: (input: {
|
|
263
|
+
readonly cursor: string | null;
|
|
264
|
+
readonly limit: number;
|
|
265
|
+
}) => Effect.Effect<UndeliveredEventPage, BillingError>;
|
|
225
266
|
}
|
|
226
267
|
|
|
268
|
+
/** Invoice ownership is resolved from native customer links before dispatch. */
|
|
269
|
+
export declare type BillingProviderEvent = Exclude<BillingEvent, {
|
|
270
|
+
readonly _tag: 'invoicePaid' | 'invoicePaymentFailed';
|
|
271
|
+
}> | BillingInvoiceTrigger;
|
|
272
|
+
|
|
227
273
|
/** The billing service Tag — `const billing = yield* BillingService`. */
|
|
228
274
|
export declare class BillingService extends BillingService_base {
|
|
229
275
|
}
|
|
@@ -266,6 +312,8 @@ export declare interface BillingServiceShape {
|
|
|
266
312
|
readonly consumeEntitlement: (tenantId: string, key: string, cost: number) => Effect.Effect<void, BillingError | EntitlementExceeded>;
|
|
267
313
|
/** Record metered usage locally; flushed to the provider in batches. */
|
|
268
314
|
readonly reportUsage: (tenantId: string, key: string, qty: number) => Effect.Effect<void, BillingError>;
|
|
315
|
+
/** Atomically retain an occurrence receipt and increment its original UTC month. */
|
|
316
|
+
readonly reportUsageEvent: (event: MeteredUsageEvent) => Effect.Effect<void, BillingError>;
|
|
269
317
|
/** Flush all pending local usage counters to the provider. */
|
|
270
318
|
readonly flushUsage: () => Effect.Effect<void, BillingError>;
|
|
271
319
|
/** What a plan/seat change WOULD cost, from the provider's own invoice
|
|
@@ -275,8 +323,27 @@ export declare interface BillingServiceShape {
|
|
|
275
323
|
readonly plan?: PlanId;
|
|
276
324
|
readonly quantity?: number;
|
|
277
325
|
}) => Effect.Effect<SubscriptionChange, BillingError>;
|
|
278
|
-
/**
|
|
326
|
+
/** Bounded recent provider invoice preview for this tenant, not history coverage. */
|
|
279
327
|
readonly invoices: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord>, BillingError>;
|
|
328
|
+
/** Read history for an exact current or retained customer owned by the tenant.
|
|
329
|
+
* Does not create custody or persist observations. The caller authorizes access. */
|
|
330
|
+
readonly invoiceHistory: (tenantId: string, providerCustomerId: string, options?: {
|
|
331
|
+
readonly cursor?: ProviderInvoiceHistoryCursor;
|
|
332
|
+
readonly limit?: number;
|
|
333
|
+
}) => Effect.Effect<ProviderInvoiceHistoryPage, BillingError>;
|
|
334
|
+
/** Fresh detailed provider evidence for this tenant's current linked customer.
|
|
335
|
+
* This read neither grants entitlements nor replaces native reconciliation. */
|
|
336
|
+
readonly invoiceSnapshot: (tenantId: string, providerInvoiceId: string) => Effect.Effect<ProviderInvoiceSnapshot, BillingError>;
|
|
337
|
+
/** Persist and read back a complete owned observation. A lost revision race
|
|
338
|
+
* repeats the provider read; event payloads never replace provider evidence. */
|
|
339
|
+
readonly reconcileInvoice: (tenantId: string, providerInvoiceId: string, expectedCustomerId?: string) => Effect.Effect<InvoiceObservation, BillingError>;
|
|
340
|
+
/** One bounded durable account/customer pass. Counts are work observations,
|
|
341
|
+
* not global, payment or historical financial completion. */
|
|
342
|
+
readonly reconcileInvoiceHistory: () => Effect.Effect<InvoiceHistoryPassResult, BillingError>;
|
|
343
|
+
/** Process one retained owned history reference with fresh provider/native
|
|
344
|
+
* reads and a fenced durable result. Open/draft/error work stays pending;
|
|
345
|
+
* an observed result is not settlement or historical entitlement coverage. */
|
|
346
|
+
readonly processInvoiceHistoryReference: (tenantId: string, providerCustomerId: string, referenceId: string) => Effect.Effect<InvoiceHistoryReferenceResult, BillingError>;
|
|
280
347
|
/** Mint a provider-hosted checkout URL for `plan`. */
|
|
281
348
|
readonly startCheckout: (input: {
|
|
282
349
|
readonly tenantId: string;
|
|
@@ -285,6 +352,9 @@ export declare interface BillingServiceShape {
|
|
|
285
352
|
readonly cancelUrl: string;
|
|
286
353
|
/** Seats to buy up front. Omit for 1. */
|
|
287
354
|
readonly quantity?: number;
|
|
355
|
+
/** Server-call authority check repeated after waits, before provider work.
|
|
356
|
+
* Native RPC routes supply their current declared-guard check. */
|
|
357
|
+
readonly authorize?: () => Effect.Effect<void, BillingError>;
|
|
288
358
|
}) => Effect.Effect<{
|
|
289
359
|
url: string;
|
|
290
360
|
}, BillingError>;
|
|
@@ -334,8 +404,8 @@ export declare interface BillingServiceShape {
|
|
|
334
404
|
* load. Every call that changes what a tenant pays already runs it first.
|
|
335
405
|
*/
|
|
336
406
|
readonly syncTenant: (tenantId: string) => Effect.Effect<Subscription | null, BillingError>;
|
|
337
|
-
/** Apply a
|
|
338
|
-
readonly applyEvent: (event:
|
|
407
|
+
/** Apply a verified provider trigger and return its native tenant-resolved event. */
|
|
408
|
+
readonly applyEvent: (event: BillingProviderEvent) => Effect.Effect<BillingEvent, BillingError>;
|
|
339
409
|
/**
|
|
340
410
|
* Change the tenant's plan mid-cycle. Computes a TIME-BASED prorated
|
|
341
411
|
* settlement on the amount difference for the unused remainder of the
|
|
@@ -373,6 +443,12 @@ export declare type CancelMode =
|
|
|
373
443
|
|
|
374
444
|
export declare interface CheckoutInput {
|
|
375
445
|
readonly tenantId: string;
|
|
446
|
+
/** Identity of one logical checkout request. Providers that support request
|
|
447
|
+
* idempotency must forward it unchanged across transport retries. The
|
|
448
|
+
* caller owns persistence and must not reuse it for different parameters. */
|
|
449
|
+
readonly idempotencyKey?: string;
|
|
450
|
+
/** Fixed deadline of a persisted intent; providers enforce it on creation. */
|
|
451
|
+
readonly expiresAt?: Date;
|
|
376
452
|
/** Provider price id to subscribe to. */
|
|
377
453
|
readonly priceId: string;
|
|
378
454
|
/** Usage-based prices added next to `priceId` (no quantity). */
|
|
@@ -393,13 +469,50 @@ export declare interface CheckoutInput {
|
|
|
393
469
|
readonly trialDays?: number;
|
|
394
470
|
}
|
|
395
471
|
|
|
472
|
+
export declare interface CheckoutSessionLookup {
|
|
473
|
+
readonly receipt: CheckoutSessionReceipt | null;
|
|
474
|
+
/** Provider-clock evidence, never the application host's clock. */
|
|
475
|
+
readonly observedAt: Date | null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Dumb adapter over a billing provider. Holds NO policy — checkout/portal
|
|
480
|
+
* URL minting, usage push, and pure payload→event mapping only. The
|
|
481
|
+
* `BillingService` wraps it with the entitlement engine, the DB rows, and
|
|
482
|
+
* transient retry.
|
|
483
|
+
*
|
|
484
|
+
* `normalizeEvent` receives a payload whose signature was ALREADY verified
|
|
485
|
+
* by @voltro/plugin-webhooks' mounter — this is pure mapping, never a
|
|
486
|
+
* second signature check.
|
|
487
|
+
*/
|
|
488
|
+
export declare interface CheckoutSessionReceipt {
|
|
489
|
+
readonly id: string;
|
|
490
|
+
readonly operationId: string | null;
|
|
491
|
+
readonly tenantId: string | null;
|
|
492
|
+
readonly providerCustomerId: string | null;
|
|
493
|
+
readonly providerSubscriptionId: string | null;
|
|
494
|
+
readonly status: 'open' | 'complete' | 'expired';
|
|
495
|
+
readonly url: string | null;
|
|
496
|
+
readonly expiresAt: Date;
|
|
497
|
+
}
|
|
498
|
+
|
|
396
499
|
/** What `closeAccount` did at the provider. */
|
|
397
500
|
export declare interface CloseAccountResult {
|
|
398
501
|
readonly tenantId: string;
|
|
399
|
-
/** Provider subscriptions
|
|
502
|
+
/** Provider subscriptions positively confirmed canceled. A scheduled close remains pending. */
|
|
400
503
|
readonly canceled: ReadonlyArray<string>;
|
|
401
504
|
}
|
|
402
505
|
|
|
506
|
+
declare interface CustomerBinding extends CustomerScope {
|
|
507
|
+
readonly tenantId: string;
|
|
508
|
+
readonly providerCustomerId: string;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
declare interface CustomerScope {
|
|
512
|
+
readonly provider: string;
|
|
513
|
+
readonly account: ProviderAccountIdentity;
|
|
514
|
+
}
|
|
515
|
+
|
|
403
516
|
/** The free/fallback plan id every app implicitly has. */
|
|
404
517
|
export declare const DEFAULT_PLAN: PlanId;
|
|
405
518
|
|
|
@@ -466,6 +579,59 @@ export declare interface EntitlementStatus {
|
|
|
466
579
|
readonly lockout: LockoutMode;
|
|
467
580
|
}
|
|
468
581
|
|
|
582
|
+
/** Immutable native ownership of one provider invoice, not payment authority. */
|
|
583
|
+
export declare interface InvoiceBinding extends ProviderAccountIdentity {
|
|
584
|
+
readonly tenantId: string;
|
|
585
|
+
readonly provider: string;
|
|
586
|
+
readonly providerCustomerId: string;
|
|
587
|
+
readonly providerInvoiceId: string;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
declare interface InvoiceHistoryJournalOwner extends CustomerBinding {
|
|
591
|
+
readonly custodyId: string;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
declare interface InvoiceHistoryPassResult {
|
|
595
|
+
readonly kind: 'idle' | 'advanced' | 'contended';
|
|
596
|
+
readonly customerCustodyId: string | null;
|
|
597
|
+
readonly admittedPages: number;
|
|
598
|
+
readonly observed: number;
|
|
599
|
+
readonly deferred: number;
|
|
600
|
+
readonly busy: number;
|
|
601
|
+
readonly unconfirmed: number;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
declare interface InvoiceHistoryReferenceResult {
|
|
605
|
+
readonly kind: 'observed' | 'deferred' | 'busy';
|
|
606
|
+
readonly work: InvoiceHistoryWork;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
declare interface InvoiceHistoryWork extends InvoiceHistoryJournalOwner {
|
|
610
|
+
readonly id: string;
|
|
611
|
+
readonly walkId: string;
|
|
612
|
+
readonly referenceId: string;
|
|
613
|
+
readonly providerInvoiceId: string;
|
|
614
|
+
readonly revision: number;
|
|
615
|
+
/** Logical attempts and acquired execution identities survive restarts. */
|
|
616
|
+
readonly attempt: number;
|
|
617
|
+
readonly executions: number;
|
|
618
|
+
readonly state: 'queued' | 'working' | 'retry' | 'deferred' | 'observed';
|
|
619
|
+
readonly activeClaimId: string | null;
|
|
620
|
+
readonly nextAttemptAt: Date;
|
|
621
|
+
readonly lastOutcome: 'unconfirmed' | 'open' | 'observed';
|
|
622
|
+
/** An observed native version, never a payment or entitlement receipt. */
|
|
623
|
+
readonly observationId: string | null;
|
|
624
|
+
readonly observationRevision: number | null;
|
|
625
|
+
readonly updatedAt: Date;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export declare interface InvoiceObservation extends InvoiceBinding {
|
|
629
|
+
readonly id: string;
|
|
630
|
+
readonly identityKey: string;
|
|
631
|
+
readonly revision: number;
|
|
632
|
+
readonly snapshot: ProviderInvoiceSnapshot;
|
|
633
|
+
}
|
|
634
|
+
|
|
469
635
|
export declare interface InvoiceRecord {
|
|
470
636
|
readonly providerInvoiceId: string;
|
|
471
637
|
readonly amountMinor: number;
|
|
@@ -488,6 +654,15 @@ export declare interface InvoiceRecord {
|
|
|
488
654
|
*/
|
|
489
655
|
export declare type LockoutMode = 'hard' | 'soft';
|
|
490
656
|
|
|
657
|
+
export declare interface MeteredUsageEvent {
|
|
658
|
+
readonly tenantId: string;
|
|
659
|
+
readonly entitlementKey: string;
|
|
660
|
+
readonly quantity: number;
|
|
661
|
+
readonly occurredAt: Date;
|
|
662
|
+
/** Stable tenant-scoped occurrence identity, reused unchanged on every retry. */
|
|
663
|
+
readonly idempotencyKey: string;
|
|
664
|
+
}
|
|
665
|
+
|
|
491
666
|
/** A monetary amount: integer minor units + ISO-4217 currency. */
|
|
492
667
|
export declare interface Money {
|
|
493
668
|
/** Integer minor units (cents). Never a float. */
|
|
@@ -529,6 +704,88 @@ export declare interface PortalInput {
|
|
|
529
704
|
readonly returnUrl: string;
|
|
530
705
|
}
|
|
531
706
|
|
|
707
|
+
/** Provider-observed account/mode, not a configured key prefix or tenant hint. */
|
|
708
|
+
export declare interface ProviderAccountIdentity {
|
|
709
|
+
readonly providerAccountId: string;
|
|
710
|
+
readonly livemode: boolean;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** A positive customer response in an explicitly expected provider namespace.
|
|
714
|
+
* observedAt is local observation time, never provider-clock settlement evidence. */
|
|
715
|
+
export declare interface ProviderCustomerSnapshot {
|
|
716
|
+
readonly account: ProviderAccountIdentity;
|
|
717
|
+
readonly providerCustomerId: string;
|
|
718
|
+
readonly deleted: boolean;
|
|
719
|
+
readonly observedAt: Date;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Explicit provider namespace/customer binding; not a bearer authorization. */
|
|
723
|
+
export declare interface ProviderInvoiceHistoryCursor extends ProviderAccountIdentity {
|
|
724
|
+
readonly providerCustomerId: string;
|
|
725
|
+
readonly afterInvoiceId: string;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** One history reference, not a complete invoice observation or payment proof. */
|
|
729
|
+
export declare interface ProviderInvoiceHistoryEntry extends InvoiceRecord {
|
|
730
|
+
readonly providerCustomerId: string;
|
|
731
|
+
readonly livemode: boolean;
|
|
732
|
+
readonly status: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void';
|
|
733
|
+
readonly createdAt: Date;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export declare interface ProviderInvoiceHistoryInput {
|
|
737
|
+
readonly account: ProviderAccountIdentity;
|
|
738
|
+
readonly providerCustomerId: string;
|
|
739
|
+
readonly cursor?: ProviderInvoiceHistoryCursor;
|
|
740
|
+
/** 1..100; defaults to 100. Invalid values fail rather than clamp. */
|
|
741
|
+
readonly limit?: number;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export declare interface ProviderInvoiceHistoryPage {
|
|
745
|
+
readonly account: ProviderAccountIdentity;
|
|
746
|
+
readonly providerCustomerId: string;
|
|
747
|
+
readonly invoices: ReadonlyArray<ProviderInvoiceHistoryEntry>;
|
|
748
|
+
readonly nextCursor: ProviderInvoiceHistoryCursor | null;
|
|
749
|
+
/** Local observation time, not provider-clock or financial coverage evidence. */
|
|
750
|
+
readonly observedAt: Date;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export declare interface ProviderInvoiceLineSnapshot {
|
|
754
|
+
readonly providerLineId: string;
|
|
755
|
+
readonly providerSubscriptionId: string | null;
|
|
756
|
+
readonly providerSubscriptionItemId: string | null;
|
|
757
|
+
readonly providerPriceId: string | null;
|
|
758
|
+
readonly amountMinor: number;
|
|
759
|
+
readonly currency: string;
|
|
760
|
+
/** Exact provider decimal; absent precision must never be replaced by a truncated integer. */
|
|
761
|
+
readonly quantityDecimal: string | null;
|
|
762
|
+
/** Provider period bounds are inclusive; they are not paid-through authority. */
|
|
763
|
+
readonly periodStart: Date;
|
|
764
|
+
readonly periodEnd: Date;
|
|
765
|
+
/** Null means the provider did not identify the line's origin. */
|
|
766
|
+
readonly proration: boolean | null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** A complete provider observation, not an entitlement or a payment receipt. */
|
|
770
|
+
export declare interface ProviderInvoiceSnapshot {
|
|
771
|
+
readonly providerInvoiceId: string;
|
|
772
|
+
readonly providerCustomerId: string;
|
|
773
|
+
readonly providerSubscriptionId: string | null;
|
|
774
|
+
readonly providerRevisionId: string | null;
|
|
775
|
+
readonly status: 'open' | 'paid' | 'uncollectible' | 'void';
|
|
776
|
+
readonly livemode: boolean;
|
|
777
|
+
readonly currency: string;
|
|
778
|
+
readonly amountDueMinor: number;
|
|
779
|
+
readonly amountPaidMinor: number;
|
|
780
|
+
readonly amountRemainingMinor: number;
|
|
781
|
+
readonly finalizedAt: Date;
|
|
782
|
+
readonly paidAt: Date | null;
|
|
783
|
+
/** Local read completion time, never a provider clock or entitlement deadline. */
|
|
784
|
+
readonly observedAt: Date;
|
|
785
|
+
/** All pages. A bounded or failed read must fail instead of returning a prefix. */
|
|
786
|
+
readonly lines: ReadonlyArray<ProviderInvoiceLineSnapshot>;
|
|
787
|
+
}
|
|
788
|
+
|
|
532
789
|
/** What `reconcileFromProvider` found and corrected. */
|
|
533
790
|
export declare interface ProviderReconcileResult {
|
|
534
791
|
/** Subscriptions compared (provider-listed plus local rows it did not list). */
|
|
@@ -643,6 +900,12 @@ export declare interface SubscriptionChangeResult {
|
|
|
643
900
|
readonly currentPeriodEnd: Date | null;
|
|
644
901
|
}
|
|
645
902
|
|
|
903
|
+
export declare interface SubscriptionCloseOperation {
|
|
904
|
+
readonly id: string;
|
|
905
|
+
/** Provider-clock deadline captured after final usage dispatch. */
|
|
906
|
+
readonly cancelAt: Date;
|
|
907
|
+
}
|
|
908
|
+
|
|
646
909
|
/**
|
|
647
910
|
* Raised when dunning has locked a tenant out — the grace period after a
|
|
648
911
|
* provider-confirmed past-due has expired. Distinct from
|
|
@@ -692,10 +955,18 @@ export declare interface SubscriptionUpdateInput {
|
|
|
692
955
|
/** An event the provider failed to deliver, reduced to what a catch-up needs. */
|
|
693
956
|
export declare interface UndeliveredEvent {
|
|
694
957
|
readonly id: string;
|
|
958
|
+
readonly event: BillingProviderEvent | null;
|
|
959
|
+
/** A recognized provider event could not be normalized; retry/report it, never fall back to discovery. */
|
|
960
|
+
readonly rejected?: true;
|
|
695
961
|
readonly providerCustomerId: string | null;
|
|
696
962
|
readonly createdAt: Date | null;
|
|
697
963
|
}
|
|
698
964
|
|
|
965
|
+
export declare interface UndeliveredEventPage {
|
|
966
|
+
readonly events: ReadonlyArray<UndeliveredEvent>;
|
|
967
|
+
readonly nextCursor: string | null;
|
|
968
|
+
}
|
|
969
|
+
|
|
699
970
|
export declare interface UsagePush {
|
|
700
971
|
readonly tenantId: string;
|
|
701
972
|
readonly entitlementKey: string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BillingError as e } from "./types.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
import { and as n, eq as r, id as i, integer as a, table as o, text as s, timestamp as c } from "@voltro/database";
|
|
4
|
+
//#region src/usageEvents.ts
|
|
5
|
+
var l = "_voltro_billing_usage_events", u = o(l, {
|
|
6
|
+
id: i({ prefix: "busage" }),
|
|
7
|
+
tenantId: s().maxLength(191),
|
|
8
|
+
idempotencyKey: s().maxLength(191),
|
|
9
|
+
entitlementKey: s().maxLength(100),
|
|
10
|
+
period: s().maxLength(7),
|
|
11
|
+
quantity: a(),
|
|
12
|
+
occurredAt: c(),
|
|
13
|
+
createdAt: c()
|
|
14
|
+
}).unique("byBillingUsageEvent", ["tenantId", "idempotencyKey"]).index("byBillingUsageEventPeriod", ["tenantId", "period"]), d = () => new e({
|
|
15
|
+
source: "usageEvent",
|
|
16
|
+
message: "Usage event identity or immutable payload requires reconciliation.",
|
|
17
|
+
transient: !1
|
|
18
|
+
}), f = (e) => {
|
|
19
|
+
if (e.tenantId.length === 0 || e.tenantId.length > 191 || e.idempotencyKey.trim().length === 0 || e.idempotencyKey.length > 191 || e.entitlementKey.length === 0 || e.entitlementKey.length > 100 || !Number.isSafeInteger(e.quantity) || e.quantity <= 0 || !(e.occurredAt instanceof Date) || !Number.isFinite(e.occurredAt.getTime())) throw d();
|
|
20
|
+
let t = new Date(e.occurredAt);
|
|
21
|
+
if (!/^[0-9]{4}-(0[1-9]|1[0-2])$/.test(e.period) || e.period !== `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, "0")}`) throw d();
|
|
22
|
+
return {
|
|
23
|
+
tenantId: e.tenantId,
|
|
24
|
+
idempotencyKey: e.idempotencyKey,
|
|
25
|
+
entitlementKey: e.entitlementKey,
|
|
26
|
+
quantity: e.quantity,
|
|
27
|
+
period: e.period,
|
|
28
|
+
occurredAt: t
|
|
29
|
+
};
|
|
30
|
+
}, p = (e, t) => e.tenantId === t.tenantId && e.idempotencyKey === t.idempotencyKey && e.entitlementKey === t.entitlementKey && e.period === t.period && e.quantity === t.quantity && new Date(e.occurredAt).getTime() === t.occurredAt.getTime(), m = (i, a, o) => t.tryPromise({
|
|
31
|
+
try: async () => {
|
|
32
|
+
let e = f(a);
|
|
33
|
+
await i.transactional(async (t) => {
|
|
34
|
+
let i = {
|
|
35
|
+
table: l,
|
|
36
|
+
predicate: n(r("tenantId", e.tenantId), r("idempotencyKey", e.idempotencyKey)),
|
|
37
|
+
order: [],
|
|
38
|
+
take: 2,
|
|
39
|
+
skip: void 0,
|
|
40
|
+
projection: void 0
|
|
41
|
+
}, a = await t.query(i);
|
|
42
|
+
if (a.length > 1) throw d();
|
|
43
|
+
if (a[0]) {
|
|
44
|
+
if (!p(a[0], e)) throw d();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let s = "busage_" + globalThis.crypto.randomUUID();
|
|
48
|
+
await t.insertIgnore(l, {
|
|
49
|
+
...e,
|
|
50
|
+
id: s,
|
|
51
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
52
|
+
}, { conflictColumns: ["tenantId", "idempotencyKey"] });
|
|
53
|
+
let c = await t.query(i);
|
|
54
|
+
if (c.length !== 1 || !c[0] || !p(c[0], e)) throw d();
|
|
55
|
+
c[0].id === s && await o(t, e);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
catch: (t) => t instanceof e ? t : new e({
|
|
59
|
+
source: "usageEvent",
|
|
60
|
+
message: "Usage event could not be durably recorded. Retry the same event identity.",
|
|
61
|
+
transient: !0
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
//#endregion
|
|
65
|
+
export { l as BILLING_USAGE_EVENTS_TABLE, u as billingUsageEventsTable, m as recordDataStoreUsageEvent };
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { makeBillingEventDelivery as e } from "./eventDelivery.js";
|
|
2
|
+
import { Effect as t, Schema as n } from "effect";
|
|
3
|
+
import { createLogger as r } from "@voltro/logger";
|
|
4
|
+
import { IdempotencyCache as i, defineIncomingWebhook as a, mountIncomingWebhook as o } from "@voltro/plugin-webhooks";
|
|
5
|
+
import { stripeProvider as s } from "@voltro/plugin-webhooks/providers";
|
|
6
|
+
//#region src/webhook.ts
|
|
7
|
+
var c = r({ scope: "@voltro/plugin-billing" }), l = "billing", u = "/billing/webhook", d = 3e5, f = (r) => {
|
|
8
|
+
let i = e(r);
|
|
9
|
+
return a({
|
|
10
|
+
id: l,
|
|
11
|
+
path: u,
|
|
12
|
+
provider: s(),
|
|
13
|
+
payload: n.Unknown,
|
|
14
|
+
handler: async (e) => {
|
|
15
|
+
let n = r.provider.verifyWebhook !== void 0 && r.webhookSecret !== null && r.webhookSecret !== void 0 ? await t.runPromise(r.provider.verifyWebhook({
|
|
16
|
+
rawBody: e.rawBody,
|
|
17
|
+
signatureHeader: e.headers["stripe-signature"] ?? "",
|
|
18
|
+
secret: r.webhookSecret ?? ""
|
|
19
|
+
})) : e.body, a = await t.runPromise(r.provider.normalizeEvent(n)), o = r.provider.eventContext?.(n) ?? {
|
|
20
|
+
providerCustomerId: null,
|
|
21
|
+
createdAt: null
|
|
22
|
+
};
|
|
23
|
+
(a !== null || o.providerCustomerId !== null) && (o.createdAt !== null && Date.now() - o.createdAt.getTime() > 3e5 && r.onLateDelivery?.(), await t.runPromise(i({
|
|
24
|
+
id: e.idempotencyKey,
|
|
25
|
+
event: a,
|
|
26
|
+
...o
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}, p = (e) => o(f(e), {
|
|
31
|
+
resolveSecret: () => Promise.resolve(e.webhookSecret),
|
|
32
|
+
idempotencyCache: e.idempotencyCache ?? new i(),
|
|
33
|
+
log: (e) => {
|
|
34
|
+
e.status >= 400 && c.warn("billing webhook rejected", { ...e });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
//#endregion
|
|
38
|
+
export { l as BILLING_WEBHOOK_ID, u as BILLING_WEBHOOK_PATH, d as REDELIVERY_AGE_MS, f as billingWebhookDescriptor, p as mountBillingWebhook };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-billing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Billing on the official Stripe SDK — subscriptions, plans, entitlements and usage metering. Proration, failed-payment retries, tax, promotion codes, trials and the checkout seat stepper are Stripe's own; the plugin adds the entitlement engine, the enforce interceptor, typed BillingError/EntitlementExceeded, the /billing/webhook mount with Stripe's signature verification, the BillingService Tag, requireEntitlement(), and a browser-safe useStartCheckout() hook. Money is integer minor units end-to-end.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"import": "./dist/rpc.js",
|
|
39
39
|
"default": "./dist/rpc.js"
|
|
40
40
|
},
|
|
41
|
+
"./economy": {
|
|
42
|
+
"types": "./dist/economy.d.ts",
|
|
43
|
+
"import": "./dist/economy.js",
|
|
44
|
+
"default": "./dist/economy.js"
|
|
45
|
+
},
|
|
41
46
|
"./package.json": "./package.json"
|
|
42
47
|
},
|
|
43
48
|
"main": "./dist/index.js",
|
|
@@ -48,20 +53,25 @@
|
|
|
48
53
|
"node": ">=24.0.0"
|
|
49
54
|
},
|
|
50
55
|
"dependencies": {
|
|
51
|
-
"@voltro/database": "0.
|
|
52
|
-
"@voltro/env": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/plugin-webhooks": "0.
|
|
55
|
-
"@voltro/protocol": "0.
|
|
56
|
+
"@voltro/database": "0.87.0",
|
|
57
|
+
"@voltro/env": "0.87.0",
|
|
58
|
+
"@voltro/logger": "0.87.0",
|
|
59
|
+
"@voltro/plugin-webhooks": "0.87.0",
|
|
60
|
+
"@voltro/protocol": "0.87.0",
|
|
61
|
+
"@voltro/runtime": "0.87.0"
|
|
56
62
|
},
|
|
57
63
|
"peerDependencies": {
|
|
58
64
|
"effect": "4.0.1",
|
|
59
65
|
"react": "^19.0.0",
|
|
60
|
-
"stripe": "^22.6.1"
|
|
66
|
+
"stripe": "^22.6.1",
|
|
67
|
+
"@voltro/plugin-economy": "0.87.0"
|
|
61
68
|
},
|
|
62
69
|
"peerDependenciesMeta": {
|
|
63
70
|
"react": {
|
|
64
71
|
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@voltro/plugin-economy": {
|
|
74
|
+
"optional": true
|
|
65
75
|
}
|
|
66
76
|
},
|
|
67
77
|
"publishConfig": {
|