@lunora/payment 1.0.0-alpha.13 → 1.0.0-alpha.131
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/LICENSE.md +6 -0
- package/README.md +8 -4
- package/dist/index.d.mts +407 -530
- package/dist/index.d.ts +407 -530
- package/dist/index.mjs +1 -16
- package/dist/packem_shared/LunoraPaymentError-BSxyhWgu.mjs +1 -0
- package/dist/packem_shared/MemoryPaymentStore-CZbrrEb1.mjs +1 -0
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-C-ICZkun.mjs +1 -0
- package/dist/packem_shared/adapter.d-DytKm_IF.d.mts +513 -0
- package/dist/packem_shared/adapter.d-DytKm_IF.d.ts +513 -0
- package/dist/packem_shared/addMoney-B4ufWAnD.mjs +1 -0
- package/dist/packem_shared/applyWebhookAction-CvLpZjKP.mjs +1 -0
- package/dist/packem_shared/constantTimeEqual-ggrwGvRa.mjs +1 -0
- package/dist/packem_shared/createAdapterRegistry-DyCsry1Q.mjs +1 -0
- package/dist/packem_shared/createDatabasePaymentStore-BGcHX1uq.mjs +1 -0
- package/dist/packem_shared/createPayment-Dimez2zY.mjs +1 -0
- package/dist/packem_shared/entitlementsForReference-PMmhAOWa.mjs +1 -0
- package/dist/packem_shared/idempotencyKey-qQJLcEQX.mjs +1 -0
- package/dist/packem_shared/json-BJJPJVYj.mjs +1 -0
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-CFPYSEu0.mjs +1 -0
- package/dist/packem_shared/not-supported-Cl0brzhn.mjs +1 -0
- package/dist/packem_shared/observability-BteZ2dJS.mjs +1 -0
- package/dist/packem_shared/paymentTables-Ve_lJxAt.mjs +1 -0
- package/dist/packem_shared/reconcile-fbxX8j9k.mjs +1 -0
- package/dist/packem_shared/subscription-event-C8GiQSFM.mjs +1 -0
- package/dist/providers/autumn-features.d.mts +129 -0
- package/dist/providers/autumn-features.d.ts +129 -0
- package/dist/providers/autumn-features.mjs +1 -0
- package/dist/providers/autumn.d.mts +28 -0
- package/dist/providers/autumn.d.ts +28 -0
- package/dist/providers/autumn.mjs +1 -0
- package/dist/providers/creem.d.mts +26 -0
- package/dist/providers/creem.d.ts +26 -0
- package/dist/providers/creem.mjs +1 -0
- package/dist/providers/dodopayments.d.mts +30 -0
- package/dist/providers/dodopayments.d.ts +30 -0
- package/dist/providers/dodopayments.mjs +1 -0
- package/dist/providers/polar.d.mts +31 -0
- package/dist/providers/polar.d.ts +31 -0
- package/dist/providers/polar.mjs +1 -0
- package/dist/providers/stripe.d.mts +34 -0
- package/dist/providers/stripe.d.ts +34 -0
- package/dist/providers/stripe.mjs +1 -0
- package/package.json +45 -4
- package/dist/packem_shared/LunoraPaymentError-B3hEzXSs.mjs +0 -22
- package/dist/packem_shared/MemoryPaymentStore-DvgdWa3C.mjs +0 -72
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs +0 -26
- package/dist/packem_shared/addMoney-bCcs1nyw.mjs +0 -60
- package/dist/packem_shared/applyWebhookAction-DpAqf3Lw.mjs +0 -177
- package/dist/packem_shared/constantTimeEqual-CfY0jYcL.mjs +0 -95
- package/dist/packem_shared/createAdapterRegistry-BuDHFCBc.mjs +0 -24
- package/dist/packem_shared/createDatabasePaymentStore-bYB_HUE6.mjs +0 -172
- package/dist/packem_shared/createPayment-BccfPGyw.mjs +0 -190
- package/dist/packem_shared/createPolarAdapter-CgErk9zo.mjs +0 -220
- package/dist/packem_shared/createStripeAdapter-oDvMhpeX.mjs +0 -279
- package/dist/packem_shared/entitlementsForReference-CzZGXPoZ.mjs +0 -51
- package/dist/packem_shared/idempotencyKey-BFzDCA7g.mjs +0 -3
- package/dist/packem_shared/json-Db337f36.mjs +0 -6
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-RlKX3Kcd.mjs +0 -29
- package/dist/packem_shared/observability-CvhJ205g.mjs +0 -11
- package/dist/packem_shared/paymentTables-DccHwWr_.mjs +0 -127
- package/dist/packem_shared/reconcile-CI1ukJF9.mjs +0 -73
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core domain types for `@lunora/payment`.
|
|
3
|
+
*
|
|
4
|
+
* The provider is a stateless translator; the store owns all state. These types are the
|
|
5
|
+
* provider-agnostic vocabulary every adapter normalizes onto.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* ISO-4217 currency code (uppercase, 3 letters). Not enumerated — provider coverage varies.
|
|
9
|
+
*/
|
|
10
|
+
type CurrencyCode = string;
|
|
11
|
+
/**
|
|
12
|
+
* Money as integer minor units + currency. Always carry the two together.
|
|
13
|
+
*
|
|
14
|
+
* `minorUnits` is a `bigint`, which is **not** JSON-serializable — cross the RPC/wire boundary
|
|
15
|
+
* with the `toMoneyJSON` / `fromMoneyJSON` helpers (see `./money`).
|
|
16
|
+
*/
|
|
17
|
+
interface Money {
|
|
18
|
+
readonly currency: CurrencyCode;
|
|
19
|
+
readonly minorUnits: bigint;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Stable provider identifier (Medusa-style). Ships Stripe/Polar/Autumn/Dodo plus Creem, an EU-friendly MoR.
|
|
23
|
+
* @experimental
|
|
24
|
+
*/
|
|
25
|
+
type ProviderId = "autumn" | "creem" | "dodopayments" | "polar" | "stripe";
|
|
26
|
+
/**
|
|
27
|
+
* What a provider can do — encoded in types so tax/UX assumptions aren't tribal knowledge.
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
interface ProviderCapabilities {
|
|
31
|
+
/** True for Polar / Lemon Squeezy / Paddle; false for Stripe (PSP) and Autumn (runs on your own Stripe). Drives tax/invoice ownership. */
|
|
32
|
+
readonly merchantOfRecord: boolean;
|
|
33
|
+
/** Native hosted customer/billing portal. */
|
|
34
|
+
readonly portal: boolean;
|
|
35
|
+
/** Usage-based / metered billing. */
|
|
36
|
+
readonly usageMetering: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Lifecycle state of a one-time payment session.
|
|
40
|
+
* @experimental
|
|
41
|
+
*/
|
|
42
|
+
type PaymentState = "authorized" | "canceled" | "captured" | "failed" | "initiated" | "partially_refunded" | "refunded";
|
|
43
|
+
/**
|
|
44
|
+
* Lifecycle state of a subscription.
|
|
45
|
+
* @experimental
|
|
46
|
+
*/
|
|
47
|
+
type SubscriptionState = "active" | "canceled" | "past_due" | "paused" | "trialing";
|
|
48
|
+
/**
|
|
49
|
+
* `Customer` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
50
|
+
* @experimental
|
|
51
|
+
*/
|
|
52
|
+
interface Customer {
|
|
53
|
+
readonly createdAt: number;
|
|
54
|
+
readonly email?: string;
|
|
55
|
+
/** Provider-side customer id. */
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly provider: ProviderId;
|
|
58
|
+
/** App-side owner the customer belongs to (user / org / workspace). Opaque to this package. */
|
|
59
|
+
readonly referenceId: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `PaymentSession` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
63
|
+
* @experimental
|
|
64
|
+
*/
|
|
65
|
+
interface PaymentSession {
|
|
66
|
+
readonly amount: Money;
|
|
67
|
+
readonly capturedAmount: Money;
|
|
68
|
+
readonly createdAt: number;
|
|
69
|
+
/** Provider-side payment / intent / session id. */
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly provider: ProviderId;
|
|
72
|
+
readonly referenceId: string;
|
|
73
|
+
readonly refundedAmount: Money;
|
|
74
|
+
readonly state: PaymentState;
|
|
75
|
+
readonly updatedAt: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `Subscription` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
79
|
+
* @experimental
|
|
80
|
+
*/
|
|
81
|
+
interface Subscription {
|
|
82
|
+
readonly cancelAtPeriodEnd: boolean;
|
|
83
|
+
readonly createdAt: number;
|
|
84
|
+
readonly currentPeriodEnd?: number;
|
|
85
|
+
/** Start of the current billing period — the window `check` sums metered usage over. */
|
|
86
|
+
readonly currentPeriodStart?: number;
|
|
87
|
+
readonly id: string;
|
|
88
|
+
/** The primary (first) price/product id — `priceIds[0]`. Display and single-item plan changes. */
|
|
89
|
+
readonly priceId: string;
|
|
90
|
+
/**
|
|
91
|
+
* EVERY price/product id the subscription bills, not just the primary one. A Stripe subscription
|
|
92
|
+
* can carry an add-on or a metered price alongside the base plan, and a customer paying for one is
|
|
93
|
+
* entitled to it — so entitlements test membership here (see `hasActivePrice`).
|
|
94
|
+
*
|
|
95
|
+
* Optional because the webhook path and any pre-existing stored row carry only `priceId`; absent
|
|
96
|
+
* reads as `[priceId]`, which is exactly right for the single-item case every other provider has.
|
|
97
|
+
*/
|
|
98
|
+
readonly priceIds?: ReadonlyArray<string>;
|
|
99
|
+
readonly provider: ProviderId;
|
|
100
|
+
readonly quantity: number;
|
|
101
|
+
readonly referenceId: string;
|
|
102
|
+
readonly state: SubscriptionState;
|
|
103
|
+
readonly updatedAt: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `CustomerRef` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
107
|
+
* @experimental
|
|
108
|
+
*/
|
|
109
|
+
interface CustomerRef {
|
|
110
|
+
readonly email?: string;
|
|
111
|
+
readonly metadata?: Record<string, string>;
|
|
112
|
+
readonly referenceId: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `CheckoutInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
116
|
+
* @experimental
|
|
117
|
+
*/
|
|
118
|
+
interface CheckoutInput {
|
|
119
|
+
readonly cancelUrl: string;
|
|
120
|
+
/**
|
|
121
|
+
* Ignored at runtime (kept for backward-compat). The provider customer is always derived from the store for the
|
|
122
|
+
* authorized `referenceId` (never caller-supplied) to prevent cross-tenant checkout attachment (IDOR).
|
|
123
|
+
* Retained on the type only for backward compatibility; setting it has no effect.
|
|
124
|
+
*/
|
|
125
|
+
readonly customerId?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Customer email, used when the reference has no provider customer yet. Some Merchant-of-Record
|
|
128
|
+
* providers (e.g. Dodo Payments) require an email to mint a customer, so pass it on first checkout.
|
|
129
|
+
*/
|
|
130
|
+
readonly email?: string;
|
|
131
|
+
/** Outbound idempotency key for the provider call; auto-derived when omitted. */
|
|
132
|
+
readonly idempotencyKey?: string;
|
|
133
|
+
readonly metadata?: Record<string, string>;
|
|
134
|
+
readonly mode: "payment" | "subscription";
|
|
135
|
+
readonly priceId: string;
|
|
136
|
+
readonly quantity?: number;
|
|
137
|
+
readonly referenceId: string;
|
|
138
|
+
readonly successUrl: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `CheckoutResult` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
142
|
+
* @experimental
|
|
143
|
+
*/
|
|
144
|
+
interface CheckoutResult {
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly provider: ProviderId;
|
|
147
|
+
readonly url: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `attach` input — subscribe a reference to a plan. A thin, plan-oriented skin over
|
|
151
|
+
* {@link CheckoutInput}: `mode` defaults to `"subscription"` (the common case), so callers pass
|
|
152
|
+
* just `{ referenceId, priceId, successUrl, cancelUrl }`.
|
|
153
|
+
* @experimental
|
|
154
|
+
*/
|
|
155
|
+
interface AttachInput extends Omit<CheckoutInput, "mode"> {
|
|
156
|
+
readonly mode?: CheckoutInput["mode"];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* `PortalInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
160
|
+
* @experimental
|
|
161
|
+
*/
|
|
162
|
+
interface PortalInput {
|
|
163
|
+
readonly customerId: string;
|
|
164
|
+
readonly returnUrl: string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A single durable usage record — one metered event for a `(referenceId, featureId)` pair.
|
|
168
|
+
* @experimental
|
|
169
|
+
*/
|
|
170
|
+
interface UsageEvent {
|
|
171
|
+
readonly createdAt: number;
|
|
172
|
+
readonly featureId: string;
|
|
173
|
+
/** Caller-stable dedupe key — recording the same key twice is a no-op (exactly-once `track`). */
|
|
174
|
+
readonly idempotencyKey: string;
|
|
175
|
+
/**
|
|
176
|
+
* How the period total absorbs this event: `"add"` (the default, and the value
|
|
177
|
+
* assumed for rows written before this field existed) increments it, `"set"`
|
|
178
|
+
* RESETS it to `quantity` and discards everything recorded earlier in the
|
|
179
|
+
* period.
|
|
180
|
+
*
|
|
181
|
+
* The ledger stays append-only either way — a `"set"` is a marker, not a
|
|
182
|
+
* computed delta — which is what makes `track({ mode: "set" })` safe to call
|
|
183
|
+
* concurrently. `PaymentStore.sumUsage` (via `foldUsage`) applies the fold.
|
|
184
|
+
*/
|
|
185
|
+
readonly mode?: "add" | "set";
|
|
186
|
+
readonly provider: ProviderId;
|
|
187
|
+
/** For `"add"`, the increment. For `"set"`, the absolute period total this event declares. */
|
|
188
|
+
readonly quantity: number;
|
|
189
|
+
readonly referenceId: string;
|
|
190
|
+
/** Whether the event was successfully forwarded to the provider's metering API. */
|
|
191
|
+
readonly reportedToProvider: boolean;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `track` input — record metered usage for a reference's feature.
|
|
195
|
+
* @experimental
|
|
196
|
+
*/
|
|
197
|
+
interface TrackInput {
|
|
198
|
+
readonly featureId: string;
|
|
199
|
+
/** Caller-supplied dedupe key; a fresh one is generated when omitted (so each call records). */
|
|
200
|
+
readonly idempotencyKey?: string;
|
|
201
|
+
/**
|
|
202
|
+
* `"add"` (default) increments usage by `quantity`; `"set"` reconciles the period total to `quantity`.
|
|
203
|
+
*
|
|
204
|
+
* Both are a single append: a `"set"` records the absolute target as a marker that the period fold
|
|
205
|
+
* resets to, rather than reading the current total and appending a delta. Concurrent `"set"` calls
|
|
206
|
+
* for the same reference therefore resolve last-writer-wins instead of over- or under-counting, and
|
|
207
|
+
* a replayed `"set"` is idempotent — neither mode needs a serialized context or a per-reference lock.
|
|
208
|
+
*
|
|
209
|
+
* `"set"` reconciles the LOCAL period total only, so `track` rejects it with `VALIDATION_ERROR` on a
|
|
210
|
+
* provider that meters usage upstream: those meters are additive and cannot take a period total, and
|
|
211
|
+
* a set that LOWERS usage has no negative delta to forward — the provider would keep billing the
|
|
212
|
+
* higher figure while the local ledger holds the lower one. Use `"add"` with a metered provider.
|
|
213
|
+
*/
|
|
214
|
+
readonly mode?: "add" | "set";
|
|
215
|
+
/**
|
|
216
|
+
* Usage amount to add, or the absolute period total when `mode` is `"set"` (defaults to `1`).
|
|
217
|
+
* Must be a non-negative safe integer — a negative value would drive the summed period usage
|
|
218
|
+
* below zero and hand the reference an unbounded metered balance, so it is rejected.
|
|
219
|
+
*/
|
|
220
|
+
readonly quantity?: number;
|
|
221
|
+
readonly referenceId: string;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Result of a `track` call.
|
|
225
|
+
* @experimental
|
|
226
|
+
*/
|
|
227
|
+
interface TrackResult {
|
|
228
|
+
/** True when this call inserted a new usage event; false when deduplicated by idempotency key. */
|
|
229
|
+
readonly recorded: boolean;
|
|
230
|
+
/** True when the event was forwarded to the provider's metering API. */
|
|
231
|
+
readonly reportedToProvider: boolean;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* `check` input — is a reference allowed something right now? Pass `featureId` to check a feature
|
|
235
|
+
* grant/allowance, or `priceId` to check active access to a product (one of the two is required).
|
|
236
|
+
* @experimental
|
|
237
|
+
*/
|
|
238
|
+
interface CheckInput {
|
|
239
|
+
/** Feature to check a grant/allowance for. Provide this **or** `priceId`. */
|
|
240
|
+
readonly featureId?: string;
|
|
241
|
+
/** Provider price/product id to check active access for. Provide this **or** `featureId`. */
|
|
242
|
+
readonly priceId?: string;
|
|
243
|
+
/** Units the caller intends to consume; the check passes only when this many remain (default `1`). */
|
|
244
|
+
readonly quantity?: number;
|
|
245
|
+
readonly referenceId: string;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Result of a `check` call.
|
|
249
|
+
* @experimental
|
|
250
|
+
*/
|
|
251
|
+
interface CheckResult {
|
|
252
|
+
/** Whether the reference may consume `quantity` units of the feature right now. */
|
|
253
|
+
readonly allowed: boolean;
|
|
254
|
+
/** Remaining units this period (`limit - used`), for metered features only. */
|
|
255
|
+
readonly balance?: number;
|
|
256
|
+
/** The plan-granted cap, for metered features only. */
|
|
257
|
+
readonly limit?: number;
|
|
258
|
+
/** True for a boolean feature granted without a numeric cap. */
|
|
259
|
+
readonly unlimited: boolean;
|
|
260
|
+
/** Usage consumed this period, for metered features only. */
|
|
261
|
+
readonly used?: number;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* One feature's resolved allowance for a reference — a {@link CheckResult} tagged with its feature.
|
|
265
|
+
* @experimental
|
|
266
|
+
*/
|
|
267
|
+
interface FeatureBalance extends CheckResult {
|
|
268
|
+
readonly featureId: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Input the adapter forwards to the provider's metering API (Stripe Meter Events / Polar ingestion).
|
|
272
|
+
* @experimental
|
|
273
|
+
*/
|
|
274
|
+
interface ReportUsageInput {
|
|
275
|
+
/** Provider customer id, when known (Stripe meter events key on it). */
|
|
276
|
+
readonly customerId?: string;
|
|
277
|
+
readonly featureId: string;
|
|
278
|
+
readonly idempotencyKey: string;
|
|
279
|
+
readonly quantity: number;
|
|
280
|
+
readonly referenceId: string;
|
|
281
|
+
/** Event time in epoch ms; defaults to now at the provider. */
|
|
282
|
+
readonly timestamp?: number;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `CaptureInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
286
|
+
* @experimental
|
|
287
|
+
*/
|
|
288
|
+
interface CaptureInput {
|
|
289
|
+
/** Partial capture amount; full capture when omitted. */
|
|
290
|
+
readonly amount?: Money;
|
|
291
|
+
readonly idempotencyKey?: string;
|
|
292
|
+
readonly sessionId: string;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* `RefundInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
296
|
+
* @experimental
|
|
297
|
+
*/
|
|
298
|
+
interface RefundInput {
|
|
299
|
+
/** Partial refund amount; full refund when omitted. */
|
|
300
|
+
readonly amount?: Money;
|
|
301
|
+
readonly idempotencyKey?: string;
|
|
302
|
+
readonly reason?: string;
|
|
303
|
+
readonly sessionId: string;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* What an adapter's `refundPayment` returns: the provider-shaped session, plus the provider's own id
|
|
307
|
+
* for the refund it just issued.
|
|
308
|
+
*
|
|
309
|
+
* `RefundResult` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
310
|
+
* @experimental
|
|
311
|
+
*/
|
|
312
|
+
interface RefundResult extends PaymentSession {
|
|
313
|
+
/**
|
|
314
|
+
* The provider accepted the refund but has NOT moved the money yet — Dodo answers
|
|
315
|
+
* `refunds.create` with `pending`/`review` and settles later via `refund.succeeded`, or never,
|
|
316
|
+
* via `refund.failed`. The facade leaves its ledger untouched for one of these and lets the
|
|
317
|
+
* confirming webhook carry the money, because a `refund.failed` reverses nothing. Absent (the
|
|
318
|
+
* default) means the refund is already settled — Stripe and Polar refund synchronously.
|
|
319
|
+
*/
|
|
320
|
+
readonly pending?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* The provider's id for THIS refund — Stripe and Polar `Refund.id`, Dodo `refund_id`. It is the
|
|
323
|
+
* identity the facade keys its local refund marker on, so two in-flight refunds of the same
|
|
324
|
+
* amount on one session stay distinct. `undefined` only where a provider reports no id, which
|
|
325
|
+
* falls the marker back to the (colliding) amount.
|
|
326
|
+
*/
|
|
327
|
+
readonly refundId?: string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* `CancelSubscriptionOptions` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
331
|
+
* @experimental
|
|
332
|
+
*/
|
|
333
|
+
interface CancelSubscriptionOptions {
|
|
334
|
+
/** Cancel at period end instead of immediately. */
|
|
335
|
+
readonly atPeriodEnd?: boolean;
|
|
336
|
+
readonly idempotencyKey?: string;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* `SubscriptionPatch` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
340
|
+
* @experimental
|
|
341
|
+
*/
|
|
342
|
+
interface SubscriptionPatch {
|
|
343
|
+
/**
|
|
344
|
+
* Override the outbound idempotency key. Honoured by the Stripe adapter only — no other provider's
|
|
345
|
+
* plan-change endpoint accepts a key at all (see the `idempotency` module docblock). Stripe
|
|
346
|
+
* otherwise derives one from the subscription and the target plan/quantity, which is stable across
|
|
347
|
+
* retries of the same intent; pass your own only to re-issue a target that was already applied and
|
|
348
|
+
* then changed away from, which a stable key would replay rather than prorate again.
|
|
349
|
+
*/
|
|
350
|
+
readonly idempotencyKey?: string;
|
|
351
|
+
readonly priceId?: string;
|
|
352
|
+
readonly quantity?: number;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Normalized webhook outcome — the *core state transition* a provider event implies.
|
|
356
|
+
* @experimental
|
|
357
|
+
*/
|
|
358
|
+
type WebhookActionType = "payment.authorized" | "payment.captured" | "payment.failed" | "payment.refunded" | "subscription.active" | "subscription.canceled" | "subscription.past_due" | "subscription.paused" | "subscription.updated" | "unhandled";
|
|
359
|
+
/**
|
|
360
|
+
* How a refund action's {@link WebhookAction.amount} should be interpreted by the sync layer.
|
|
361
|
+
*
|
|
362
|
+
* `"delta"` is an incremental amount added to the running refunded total (Polar `refund.created`,
|
|
363
|
+
* and the historical default), so multiple events accumulate. `"absolute"` is the provider's
|
|
364
|
+
* cumulative refunded-to-date total (Stripe `charge.refunded` carries `amount_refunded`, which
|
|
365
|
+
* already sums all prior partial refunds); the sync layer sets the refunded total to this value
|
|
366
|
+
* rather than adding, so repeated partial-refund events do not over-count.
|
|
367
|
+
*
|
|
368
|
+
* Omitted means `"delta"`, preserving the original behavior for callers that predate this field.
|
|
369
|
+
* @experimental
|
|
370
|
+
*/
|
|
371
|
+
type RefundAmountKind = "absolute" | "delta";
|
|
372
|
+
/**
|
|
373
|
+
* `WebhookAction` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
374
|
+
* @experimental
|
|
375
|
+
*/
|
|
376
|
+
interface WebhookAction {
|
|
377
|
+
readonly amount?: Money;
|
|
378
|
+
/**
|
|
379
|
+
* Interpretation of {@link WebhookAction.amount} for refund actions (`payment.refunded`).
|
|
380
|
+
* Defaults to `"delta"` when omitted. Ignored for non-refund actions.
|
|
381
|
+
*/
|
|
382
|
+
readonly amountKind?: RefundAmountKind;
|
|
383
|
+
readonly cancelAtPeriodEnd?: boolean;
|
|
384
|
+
readonly currentPeriodEnd?: number;
|
|
385
|
+
readonly currentPeriodStart?: number;
|
|
386
|
+
readonly customerId?: string;
|
|
387
|
+
/** Provider event id — the inbound idempotency key. */
|
|
388
|
+
readonly eventId: string;
|
|
389
|
+
readonly priceId?: string;
|
|
390
|
+
/**
|
|
391
|
+
* EVERY price/product id the subscription bills, when the adapter could establish the whole set.
|
|
392
|
+
* `sync.ts` applies it as a WHOLESALE replacement, so it must be complete or absent — never a
|
|
393
|
+
* subset, which would silently drop prices the stored row already had.
|
|
394
|
+
*
|
|
395
|
+
* `undefined` leaves the stored set standing (see {@link Subscription.priceIds}). That is the
|
|
396
|
+
* single-price providers' case, and the fail-closed answer for a provider whose embedded item
|
|
397
|
+
* list is paginated and whose event carries only the first page.
|
|
398
|
+
*/
|
|
399
|
+
readonly priceIds?: ReadonlyArray<string>;
|
|
400
|
+
readonly provider: ProviderId;
|
|
401
|
+
readonly quantity?: number;
|
|
402
|
+
/** Raw provider event, retained for the events log / debugging. */
|
|
403
|
+
readonly raw?: unknown;
|
|
404
|
+
readonly referenceId?: string;
|
|
405
|
+
/**
|
|
406
|
+
* Provider id of the refund this event reports (refund actions only). Carries the per-refund
|
|
407
|
+
* identity the sync layer matches against the marker `refundPayment` left behind, so a second
|
|
408
|
+
* facade refund of the same amount is not mistaken for the first one's confirmation.
|
|
409
|
+
*/
|
|
410
|
+
readonly refundId?: string;
|
|
411
|
+
readonly sessionId?: string;
|
|
412
|
+
readonly subscriptionId?: string;
|
|
413
|
+
readonly type: WebhookActionType;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Result of applying a webhook action to the store.
|
|
417
|
+
* @experimental
|
|
418
|
+
*/
|
|
419
|
+
interface ApplyResult {
|
|
420
|
+
readonly applied: boolean;
|
|
421
|
+
readonly reason?: "duplicate" | "illegal_transition" | "invalid_refund_amount" | "ok" | "orphaned" | "unhandled";
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* A read-only header bag; the platform `Headers` object satisfies it.
|
|
425
|
+
* @experimental
|
|
426
|
+
*/
|
|
427
|
+
interface WebhookHeaders {
|
|
428
|
+
get: (name: string) => null | string;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* `WebhookInput` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
432
|
+
* @experimental
|
|
433
|
+
*/
|
|
434
|
+
interface WebhookInput {
|
|
435
|
+
/** Request headers (signature schemes read provider-specific headers from here). */
|
|
436
|
+
readonly headers: WebhookHeaders;
|
|
437
|
+
/** Raw request body, exactly as received (required for signature verification). */
|
|
438
|
+
readonly payload: string;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* A stateless translator between the provider API and Lunora's normalized vocabulary.
|
|
442
|
+
*
|
|
443
|
+
* Adapters never own state — they make provider calls and normalize provider events into a
|
|
444
|
+
* `WebhookAction`. All durable state lives in the payment store.
|
|
445
|
+
* @experimental
|
|
446
|
+
*/
|
|
447
|
+
interface PaymentAdapter {
|
|
448
|
+
cancelPayment: (sessionId: string, options?: {
|
|
449
|
+
idempotencyKey?: string;
|
|
450
|
+
}) => Promise<PaymentSession>;
|
|
451
|
+
cancelSubscription: (subscriptionId: string, options?: CancelSubscriptionOptions) => Promise<Subscription>;
|
|
452
|
+
readonly capabilities: ProviderCapabilities;
|
|
453
|
+
capturePayment: (input: CaptureInput) => Promise<PaymentSession>;
|
|
454
|
+
/**
|
|
455
|
+
* Ask the provider whether a reference may consume `quantity` units of a feature (or holds active
|
|
456
|
+
* access to a product) right now — for providers that own entitlement truth themselves (e.g.
|
|
457
|
+
* Autumn computes balances, credits, and limits from its plan config). Optional: when absent, the
|
|
458
|
+
* facade's `check` evaluates locally from the synced store + the app's `entitlements` config. When
|
|
459
|
+
* present, the facade delegates `check` to it, so `entitlements` need not be configured.
|
|
460
|
+
*/
|
|
461
|
+
checkEntitlement?: (input: CheckInput) => Promise<CheckResult>;
|
|
462
|
+
createCheckout: (input: CheckoutInput) => Promise<CheckoutResult>;
|
|
463
|
+
createPortalSession: (input: PortalInput) => Promise<{
|
|
464
|
+
url: string;
|
|
465
|
+
}>;
|
|
466
|
+
/**
|
|
467
|
+
* Resolve every feature allowance for a reference straight from the provider — the optional
|
|
468
|
+
* companion to `checkEntitlement` that powers `listBalances`. Present only on providers that
|
|
469
|
+
* own entitlement truth; when absent, the facade evaluates balances locally from the store + the
|
|
470
|
+
* app's `entitlements` config.
|
|
471
|
+
*/
|
|
472
|
+
getBalances?: (referenceId: string) => Promise<FeatureBalance[]>;
|
|
473
|
+
getOrCreateCustomer: (ref: CustomerRef) => Promise<Customer>;
|
|
474
|
+
/** Fetch the provider's current truth for a payment session — the basis for reconciliation. */
|
|
475
|
+
getPaymentStatus: (sessionId: string) => Promise<PaymentSession>;
|
|
476
|
+
/** Fetch the provider's current truth for a subscription — the basis for reconciliation. */
|
|
477
|
+
getSubscriptionStatus: (subscriptionId: string) => Promise<Subscription>;
|
|
478
|
+
/** Stable provider identifier (Medusa-style). */
|
|
479
|
+
readonly identifier: ProviderId;
|
|
480
|
+
/** Verify the signature over the raw body, then normalize the event. Throws on invalid signature. */
|
|
481
|
+
parseWebhook: (input: WebhookInput) => Promise<WebhookAction>;
|
|
482
|
+
/** Issue the refund and report the provider's id for it (see {@link RefundResult.refundId}). */
|
|
483
|
+
refundPayment: (input: RefundInput) => Promise<RefundResult>;
|
|
484
|
+
/**
|
|
485
|
+
* Forward metered usage to the provider's billing API. Optional — present only on providers
|
|
486
|
+
* whose `capabilities.usageMetering` is `true` and that expose an ingestion endpoint. When
|
|
487
|
+
* absent, `track` still records usage durably and `check` enforces limits locally.
|
|
488
|
+
*/
|
|
489
|
+
reportUsage?: (input: ReportUsageInput) => Promise<void>;
|
|
490
|
+
/**
|
|
491
|
+
* Clear a pending cancellation. `options.idempotencyKey` overrides the adapter's own stable key
|
|
492
|
+
* and is honoured by the Stripe adapter only — no other provider's endpoint accepts one.
|
|
493
|
+
*/
|
|
494
|
+
resumeSubscription: (subscriptionId: string, options?: {
|
|
495
|
+
idempotencyKey?: string;
|
|
496
|
+
}) => Promise<Subscription>;
|
|
497
|
+
updateSubscription: (subscriptionId: string, patch: SubscriptionPatch) => Promise<Subscription>;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Registry of adapters keyed by provider id — supports dual-register during provider migration.
|
|
501
|
+
* @experimental
|
|
502
|
+
*/
|
|
503
|
+
interface AdapterRegistry {
|
|
504
|
+
all: () => PaymentAdapter[];
|
|
505
|
+
get: (provider: ProviderId) => PaymentAdapter;
|
|
506
|
+
has: (provider: ProviderId) => boolean;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* `createAdapterRegistry` is part of the experimental `@lunora/payment` API and may change without a major version bump.
|
|
510
|
+
* @experimental
|
|
511
|
+
*/
|
|
512
|
+
declare const createAdapterRegistry: (adapters: ReadonlyArray<PaymentAdapter>) => AdapterRegistry;
|
|
513
|
+
export { ApplyResult as A, Customer as C, FeatureBalance as F, Money as M, PaymentAdapter as P, RefundInput as R, Subscription as S, TrackInput as T, UsageEvent as U, WebhookActionType as W, ProviderId as a, PaymentSession as b, AttachInput as c, CheckoutResult as d, CancelSubscriptionOptions as e, CaptureInput as f, CheckInput as g, CheckResult as h, CheckoutInput as i, TrackResult as j, CurrencyCode as k, PaymentState as l, SubscriptionState as m, WebhookAction as n, AdapterRegistry as o, CustomerRef as p, PortalInput as q, ProviderCapabilities as r, RefundAmountKind as s, RefundResult as t, ReportUsageInput as u, SubscriptionPatch as v, WebhookHeaders as w, WebhookInput as x, createAdapterRegistry as y };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toSnapshot as m}from"dinero.js";import{add as a,allocate as p,compare as M,subtract as f,dinero as D}from"dinero.js/bigint";import{LunoraPaymentError as y}from"./LunoraPaymentError-BSxyhWgu.mjs";const C=new Set(["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]),U=new Set(["BHD","IQD","JOD","KWD","LYD","OMR","TND"]),o=n=>C.has(n)?0n:U.has(n)?3n:2n,F=n=>({base:10n,code:n.toUpperCase(),exponent:o(n.toUpperCase())}),e=n=>D({amount:n.minorUnits,currency:F(n.currency)}),c=n=>{const r=m(n);return{currency:r.currency.code,minorUnits:r.amount}},s=(n,r)=>{if(n.currency!==r.currency)throw new y("CURRENCY_MISMATCH",`cannot combine ${n.currency} with ${r.currency}`)},R=n=>o(n.toUpperCase())===0n,i=(n,r)=>{if(typeof n=="number"&&!Number.isSafeInteger(n))throw new y("VALIDATION_ERROR",`money(): \`minorUnits\` must be a bigint or a safe integer (got ${String(n)})`);const t=typeof n=="bigint"?n:BigInt(n);return{currency:r.toUpperCase(),minorUnits:t}},g=n=>i(0n,n),b=(n,r="en-US")=>{const t=Number(o(n.currency.toUpperCase())),u=Number(n.minorUnits)/10**t;try{return new Intl.NumberFormat(r,{currency:n.currency,style:"currency"}).format(u)}catch{return`${u.toFixed(t)} ${n.currency}`}},h=(n,r)=>(s(n,r),c(a(e(n),e(r)))),w=(n,r)=>(s(n,r),c(f(e(n),e(r)))),E=(n,r)=>(s(n,r),M(e(n),e(r))),O=(n,r)=>p(e(n),[...r]).map(t=>c(t)),A=n=>n.minorUnits===0n,d=n=>({currency:n.currency,minorUnits:n.minorUnits.toString()}),L=n=>i(BigInt(n.minorUnits),n.currency);export{h as addMoney,O as allocateMoney,E as compareMoney,b as formatMoney,L as fromMoneyJSON,R as isZeroDecimalCurrency,A as isZeroMoney,i as money,w as subtractMoney,d as toMoneyJSON,g as zeroMoney};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraPaymentError as P}from"./LunoraPaymentError-BSxyhWgu.mjs";import{localRefundKey as A,LOCAL_REFUND_CLAIM_TYPE as E}from"./idempotencyKey-qQJLcEQX.mjs";import{zeroMoney as o,compareMoney as l,addMoney as _}from"./addMoney-B4ufWAnD.mjs";import{n as a}from"./observability-BteZ2dJS.mjs";import{nextPaymentState as S,nextSubscriptionState as w}from"./PAYMENT_TERMINAL_STATES-C-ICZkun.mjs";const b={"payment.authorized":"authorize","payment.captured":"capture","payment.failed":"fail","payment.refunded":"refund"},h={"subscription.active":"active","subscription.canceled":"canceled","subscription.past_due":"past_due","subscription.paused":"paused"},T="orphan-retry:",R="marker.orphan_retry",k={"subscription.active":"activate","subscription.canceled":"cancel","subscription.past_due":"mark_past_due","subscription.paused":"pause"},O=(t,e)=>l(t,e)>0?t:e,Y=new Set(["authorized","initiated"]),N=async(t,e,r)=>{if(!r||!e.sessionId||!e.amount||e.amountKind==="absolute")return e;const d=A(e.sessionId,e.refundId,e.amount),n=await t.markEventProcessed(e.provider,d,E);return await t.releaseEvent(e.provider,d),n?e:{...e,amount:o(e.amount.currency)}},v=(t,e)=>{if(!e.amount||t.refundedAmount.currency!==e.amount.currency||t.capturedAmount.currency!==e.amount.currency)return;const r=e.amountKind==="absolute"?O(e.amount,t.refundedAmount):_(t.refundedAmount,e.amount);if(!(l(r,t.capturedAmount)>0))return r},M=(t,e)=>{if(!t)return"refund";const r=v(t,e);return r&&l(r,t.capturedAmount)<0?"partial_refund":"refund"},q=async(t,e,r)=>{if(!e.sessionId)return{applied:!1,reason:"unhandled"};const d=await t.getPaymentSession(e.provider,e.sessionId),n=d?.state??"initiated",u=Date.now(),s=e.amount?.currency??d?.amount.currency??"USD",c=r==="refund"?await N(t,e,d):e,p=r==="refund"?M(d,c):r,I=S(n,p);if(!I)return{applied:!1,reason:r==="refund"&&Y.has(n)?"orphaned":"illegal_transition"};const i=d??{amount:e.amount??o(s),capturedAmount:o(s),createdAt:u,id:e.sessionId,provider:e.provider,referenceId:e.referenceId??"",refundedAmount:o(s),state:n,updatedAt:u};let{capturedAmount:m,refundedAmount:y}=i;if(p==="capture"&&e.amount&&(m=e.amount),(p==="partial_refund"||p==="refund")&&c.amount){const f=v(i,c);if(!f)return{applied:!1,reason:"invalid_refund_amount"};y=f}return await t.upsertPaymentSession({...i,capturedAmount:m,referenceId:e.referenceId??i.referenceId,refundedAmount:y,state:I,updatedAt:u}),{applied:!0,reason:"ok"}},C=(t,e,r)=>{if(e==="active"){if(t==="active")return"renew";if(t==="paused")return"resume"}return k[r]},g=async(t,e)=>{if(!e.subscriptionId)return{applied:!1,reason:"unhandled"};const r=await t.getSubscription(e.provider,e.subscriptionId),d=Date.now();if(e.type==="subscription.updated")return r?(await t.upsertSubscription({...r,cancelAtPeriodEnd:e.cancelAtPeriodEnd??r.cancelAtPeriodEnd,currentPeriodEnd:e.currentPeriodEnd??r.currentPeriodEnd,currentPeriodStart:e.currentPeriodStart??r.currentPeriodStart,priceId:e.priceId??r.priceId,priceIds:e.priceIds??r.priceIds,quantity:e.quantity??r.quantity,updatedAt:d}),{applied:!0,reason:"ok"}):{applied:!1,reason:"orphaned"};const n=h[e.type];if(!n)return{applied:!1,reason:"unhandled"};if(!r)return await t.upsertSubscription({cancelAtPeriodEnd:e.cancelAtPeriodEnd??!1,createdAt:d,currentPeriodEnd:e.currentPeriodEnd,currentPeriodStart:e.currentPeriodStart??d,id:e.subscriptionId,priceId:e.priceId??"",priceIds:e.priceIds,provider:e.provider,quantity:e.quantity??1,referenceId:e.referenceId??"",state:n,updatedAt:d}),{applied:!0,reason:"ok"};const u=C(r.state,n,e.type),s=u?w(r.state,u):void 0;return s?(await t.upsertSubscription({...r,cancelAtPeriodEnd:e.cancelAtPeriodEnd??r.cancelAtPeriodEnd,currentPeriodEnd:e.currentPeriodEnd??r.currentPeriodEnd,currentPeriodStart:e.currentPeriodStart??r.currentPeriodStart,priceId:e.priceId??r.priceId,priceIds:e.priceIds??r.priceIds,quantity:e.quantity??r.quantity,state:s,updatedAt:d}),{applied:!0,reason:"ok"}):{applied:!1,reason:"illegal_transition"}},U=async(t,e,r)=>{if(e.type==="unhandled")return{applied:!1,reason:"unhandled"};if(!e.eventId?.trim())throw new P("WEBHOOK_EVENT_ID_MISSING",`webhook event id is missing or blank for provider "${e.provider}"`);if(!await t.markEventProcessed(e.provider,e.eventId,e.type))return a(r,{eventId:e.eventId,provider:e.provider,type:"webhook.duplicate"}),{applied:!1,reason:"duplicate"};const n=b[e.type];let u;try{u=n?await q(t,e,n):await g(t,e)}catch(s){throw await t.releaseEvent(e.provider,e.eventId),s}return u.reason==="orphaned"&&(await t.markEventProcessed(e.provider,`${T}${e.eventId}`,R)?await t.releaseEvent(e.provider,e.eventId):u={applied:!1,reason:"unhandled"}),a(r,{action:e.type,eventId:e.eventId,provider:e.provider,reason:u.reason,type:"webhook.applied"}),e.type==="payment.failed"?a(r,{provider:e.provider,referenceId:e.referenceId,sessionId:e.sessionId,type:"payment.failed"}):e.type==="subscription.past_due"&&a(r,{provider:e.provider,referenceId:e.referenceId,subscriptionId:e.subscriptionId,type:"subscription.past_due"}),u};export{U as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraPaymentError as s}from"./LunoraPaymentError-BSxyhWgu.mjs";const l=e=>{let t="";for(let o=0;o<e.length;o+=32768)t+=String.fromCharCode(...e.subarray(o,o+32768));return btoa(t)},I=e=>{const t=atob(e),n=new Uint8Array(t.length);for(let o=0;o<t.length;o+=1)n[o]=t.codePointAt(o)??0;return n},f=(e,t)=>{const n=Math.max(e.length,t.length);let o=e.length^t.length;for(let r=0;r<n;r+=1){const a=r<e.length?e.charCodeAt(r):0,i=r<t.length?t.charCodeAt(r):0;o|=a^i}return o===0},h=new TextEncoder,A=e=>[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join(""),d="whsec_",y=async(e,t)=>{const n=await crypto.subtle.importKey("raw",e,{hash:"SHA-256",name:"HMAC"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",n,h.encode(t));return l(new Uint8Array(o))},m=e=>{if(!e)throw new s("CONFIG_INVALID","webhook secret not configured")},g=f,b=async(e,t)=>{const n=await crypto.subtle.importKey("raw",h.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign"]),o=await crypto.subtle.sign("HMAC",n,h.encode(t));return A(o)},u=async e=>{m(e.secret);const t=e.toleranceSeconds??300,n=e.now??Date.now(),o=Number(e.webhookTimestamp);if(!e.webhookId||!e.webhookSignature||!Number.isFinite(o))throw new s("WEBHOOK_SIGNATURE_INVALID","missing standard-webhooks headers");if(Math.abs(Math.floor(n/1e3)-o)>t)throw new s("WEBHOOK_TIMESTAMP_INVALID","signature timestamp outside tolerance");const r=e.secret.startsWith(d)?e.secret.slice(d.length):e.secret;if(!r)throw new s("CONFIG_INVALID","webhook secret not configured");const a=I(r);if(a.length===0)throw new s("CONFIG_INVALID","webhook secret not configured");const i=await y(a,`${e.webhookId}.${e.webhookTimestamp}.${e.payload}`);if(!e.webhookSignature.split(" ").map(c=>{const w=c.indexOf(",");return w===-1?"":c.slice(w+1)}).filter(Boolean).some(c=>g(c,i)))throw new s("WEBHOOK_SIGNATURE_INVALID","no matching signature")},N=async e=>{if(m(e.secret),!e.signature)throw new s("WEBHOOK_SIGNATURE_INVALID","missing creem-signature header");const t=await b(e.secret,e.payload);if(!g(e.signature,t))throw new s("WEBHOOK_SIGNATURE_INVALID","no matching signature")};export{m as assertWebhookSecret,g as constantTimeEqual,b as hmacSha256Hex,N as verifyCreemSignature,u as verifyStandardWebhook};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraPaymentError as a}from"./LunoraPaymentError-BSxyhWgu.mjs";const n=o=>{const r=new Map;for(const e of o){if(r.has(e.identifier))throw new a("CONFIG_INVALID",`duplicate adapter for provider "${e.identifier}"`);r.set(e.identifier,e)}return{all:()=>[...r.values()],get:e=>{const t=r.get(e);if(!t)throw new a("CONFIG_INVALID",`no adapter registered for provider "${e}"`);return t},has:e=>r.has(e)}};export{n as createAdapterRegistry};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{money as y}from"./addMoney-B4ufWAnD.mjs";import{foldUsage as m}from"./MemoryPaymentStore-CZbrrEb1.mjs";const o=(e,n)=>typeof e[n]=="string"?e[n]:"",l=(e,n)=>typeof e[n]=="string"?e[n]:void 0,c=(e,n)=>typeof e[n]=="number"?e[n]:0,v=(e,n)=>typeof e[n]=="number"?e[n]:void 0,A=(e,n)=>e[n]===!0,S=(e,n)=>{const t=e[n];return Array.isArray(t)?t.filter(d=>typeof d=="string"):void 0},f=(e,n)=>{const t=e[n];return typeof t=="bigint"?t:typeof t=="number"||typeof t=="string"?BigInt(t):0n},P=e=>({createdAt:e.createdAt,email:e.email,provider:e.provider,providerCustomerId:e.id,referenceId:e.referenceId}),E=e=>({createdAt:c(e,"createdAt"),email:l(e,"email"),id:o(e,"providerCustomerId"),provider:o(e,"provider"),referenceId:o(e,"referenceId")}),M=e=>({amountMinor:e.amount.minorUnits,capturedMinor:e.capturedAmount.minorUnits,createdAt:e.createdAt,currency:e.amount.currency,provider:e.provider,providerSessionId:e.id,referenceId:e.referenceId,refundedMinor:e.refundedAmount.minorUnits,state:e.state,updatedAt:e.updatedAt}),T=e=>{const n=o(e,"currency");return{amount:y(f(e,"amountMinor"),n),capturedAmount:y(f(e,"capturedMinor"),n),createdAt:c(e,"createdAt"),id:o(e,"providerSessionId"),provider:o(e,"provider"),referenceId:o(e,"referenceId"),refundedAmount:y(f(e,"refundedMinor"),n),state:o(e,"state"),updatedAt:c(e,"updatedAt")}},q=e=>({cancelAtPeriodEnd:e.cancelAtPeriodEnd,createdAt:e.createdAt,currentPeriodEnd:e.currentPeriodEnd,currentPeriodStart:e.currentPeriodStart,priceId:e.priceId,priceIds:e.priceIds===void 0?void 0:[...e.priceIds],provider:e.provider,providerSubscriptionId:e.id,quantity:e.quantity,referenceId:e.referenceId,state:e.state,updatedAt:e.updatedAt}),I=e=>({cancelAtPeriodEnd:A(e,"cancelAtPeriodEnd"),createdAt:c(e,"createdAt"),currentPeriodEnd:v(e,"currentPeriodEnd"),currentPeriodStart:v(e,"currentPeriodStart"),id:o(e,"providerSubscriptionId"),priceId:o(e,"priceId"),priceIds:S(e,"priceIds"),provider:o(e,"provider"),quantity:c(e,"quantity"),referenceId:o(e,"referenceId"),state:o(e,"state"),updatedAt:c(e,"updatedAt")}),K=e=>({createdAt:c(e,"createdAt"),featureId:o(e,"featureId"),idempotencyKey:o(e,"idempotencyKey"),mode:e.mode==="set"?"set":"add",provider:o(e,"provider"),quantity:c(e,"quantity"),referenceId:o(e,"referenceId"),reportedToProvider:A(e,"reportedToProvider")}),U=e=>({createdAt:e.createdAt,featureId:e.featureId,idempotencyKey:e.idempotencyKey,...e.mode==="set"?{mode:"set"}:{},provider:e.provider,quantity:e.quantity,referenceId:e.referenceId,reportedToProvider:e.reportedToProvider}),h=e=>{const n=async(t,d,r)=>{const a=await e.findFirst(t,d);if(a){await e.patch(a._id,r);return}await e.insert(t,r)};return{getCustomerByReference:async(t,d)=>{const r=await e.findFirst("customers",{provider:t,referenceId:d});return r?E(r):void 0},getPaymentSession:async(t,d)=>{const r=await e.findFirst("paymentSessions",{provider:t,providerSessionId:d});return r?T(r):void 0},getSubscription:async(t,d)=>{const r=await e.findFirst("subscriptions",{provider:t,providerSubscriptionId:d});return r?I(r):void 0},listSubscriptionsByReference:async t=>{const{rows:d}=await e.findMany("subscriptions",{referenceId:t});return d.map(r=>I(r))},listUnreportedUsage:async(t,d)=>{const r=Math.max(0,Math.floor(d));if(r===0)return[];const a=[{createdAt:"asc"},{idempotencyKey:"asc"}],s=[];let i;do{const u=await e.findMany("usageEvents",{provider:t,reportedToProvider:!1},{cursor:i,limit:r,orderBy:a});for(const g of u.rows){const p=K(g);if(p.mode!=="set"&&p.quantity>0&&(s.push(p),s.length===r))return s}i=u.cursor}while(i!==void 0);return s},markEventProcessed:async(t,d,r)=>await e.findFirst("events",{provider:t,providerEventId:d})?!1:(await e.insert("events",{processedAt:Date.now(),provider:t,providerEventId:d,type:r}),!0),releaseEvent:async(t,d)=>{const r=await e.findFirst("events",{provider:t,providerEventId:d});r&&await e.delete(r._id)},markUsageReported:async(t,d)=>{const r=await e.findFirst("usageEvents",{idempotencyKey:d,provider:t});r&&await e.patch(r._id,{reportedToProvider:!0})},recordUsage:async t=>await e.findFirst("usageEvents",{idempotencyKey:t.idempotencyKey,provider:t.provider})?!1:(await e.insert("usageEvents",U(t)),!0),sumUsage:async(t,d,r)=>{const{rows:a}=await e.findMany("usageEvents",{featureId:d,referenceId:t}),s=a.filter(i=>c(i,"createdAt")>=r).map(i=>({createdAt:c(i,"createdAt"),idempotencyKey:typeof i.idempotencyKey=="string"?i.idempotencyKey:"",mode:i.mode==="set"?"set":"add",quantity:c(i,"quantity")}));return m(s)},sumUsageByFeature:async(t,d,r)=>{const{rows:a}=await e.findMany("usageEvents",{referenceId:t}),s=new Map(d.map(i=>[i,[]]));for(const i of a)c(i,"createdAt")<r||s.get(typeof i.featureId=="string"?i.featureId:"")?.push({createdAt:c(i,"createdAt"),idempotencyKey:typeof i.idempotencyKey=="string"?i.idempotencyKey:"",mode:i.mode==="set"?"set":"add",quantity:c(i,"quantity")});return new Map([...s].map(([i,u])=>[i,m(u)]))},upsertCustomer:async t=>n("customers",{provider:t.provider,referenceId:t.referenceId},P(t)),upsertPaymentSession:async t=>n("paymentSessions",{provider:t.provider,providerSessionId:t.id},M(t)),upsertSubscription:async t=>n("subscriptions",{provider:t.provider,providerSubscriptionId:t.id},q(t))}};export{h as createDatabasePaymentStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toErrorBody as N}from"@lunora/errors";import{usagePeriodStart as g,resolveEntitlements as A,featureNames as U,hasActivePrice as _}from"./entitlementsForReference-PMmhAOWa.mjs";import{LunoraPaymentError as f}from"./LunoraPaymentError-BSxyhWgu.mjs";import{derivedIdempotencyKey as b,localRefundKey as D,LOCAL_REFUND_CLAIM_TYPE as E,idempotencyKey as k}from"./idempotencyKey-qQJLcEQX.mjs";import{subtractMoney as K,addMoney as T,compareMoney as v,isZeroMoney as B}from"./addMoney-B4ufWAnD.mjs";import{n as C}from"./observability-BteZ2dJS.mjs";import F from"./applyWebhookAction-CvLpZjKP.mjs";const y=(o,n=200,s)=>{const u=new Headers({"content-type":"application/json"});for(const[l,w]of Object.entries({}))u.set(l,w);return Response.json(o,{headers:u,status:n})};let h=0;const $=()=>{const o=Date.now();return h=o>h?o:h+1,h},O=o=>o?`${o.currency}:${String(o.minorUnits)}`:"full",q=o=>o&&"referenceId"in o?Object.fromEntries(Object.entries(o).filter(([n])=>n!=="referenceId")):o,G=o=>y({applied:o.applied},o.status),H=o=>{const{adapter:n,store:s}=o,u=async e=>{if(!o.authorize)return;let t;try{t=await o.authorize(e)}catch{throw new f("FORBIDDEN",`caller not authorized for reference "${e}"`)}if(t!==!0)throw new f("FORBIDDEN",`caller not authorized for reference "${e}"`)},l=async e=>{await u(e.referenceId);const t=q(e.metadata);let r;const a=await s.getCustomerByReference(n.identifier,e.referenceId);if(a)r=a.id;else{const d=await n.getOrCreateCustomer({email:e.email,referenceId:e.referenceId});r=d.id,await s.upsertCustomer(d)}const i=e.idempotencyKey??await b("checkout",n.identifier,e.referenceId,e.priceId,e.mode,String(e.quantity??1),e.successUrl,e.cancelUrl,t?JSON.stringify(t):"");return n.createCheckout({...e,customerId:r,idempotencyKey:i,metadata:t})},w=(e,t,r)=>{const a=e-t;return{allowed:a>=r,balance:a,limit:e,unlimited:!1,used:t}},S=async(e,t,r,a,i)=>{const d=e.limit(a);if(d!==void 0){const c=await s.sumUsage(r,a,g(t));return w(d,c,i)}return{allowed:e.has(a),unlimited:e.has(a)}},p=async e=>{const t=()=>new f("NOT_FOUND",`payment session "${e}" not found`);let r=await s.getPaymentSession(n.identifier,e);if(!r)try{r=await n.getPaymentStatus(e)}catch{throw t()}if(!r.referenceId)throw t();try{await u(r.referenceId)}catch{throw t()}return r},I=async(e,t)=>{const r={...e,...t,updatedAt:Date.now()};return await s.upsertPaymentSession(r),r};return{adapter:n,attach:async e=>l({...e,mode:e.mode??"subscription"}),cancelPayment:async(e,t)=>{const r=await p(e),a=t?.idempotencyKey??k("cancel_payment",n.identifier,e),i=await n.cancelPayment(e,{...t,idempotencyKey:a});return I(r,{state:i.state})},cancelSubscription:async(e,t)=>{const r=await s.getSubscription(n.identifier,e);if(!r)throw new f("NOT_FOUND",`subscription "${e}" not found`);try{await u(r.referenceId)}catch{throw new f("NOT_FOUND",`subscription "${e}" not found`)}const a=t?.idempotencyKey??k("cancel_subscription",n.identifier,e),i=await n.cancelSubscription(e,{...t,idempotencyKey:a}),d={...i,referenceId:r.referenceId===""?i.referenceId:r.referenceId};return await s.upsertSubscription(d),d},capturePayment:async e=>{const t=await p(e.sessionId),r=e.idempotencyKey??await b("capture_payment",n.identifier,e.sessionId,O(e.amount)),a=await n.capturePayment({...e,idempotencyKey:r});return I(t,{capturedAmount:a.capturedAmount,state:a.state})},check:async e=>{if(await u(e.referenceId),e.featureId===void 0&&e.priceId===void 0)throw new f("VALIDATION_ERROR","check() requires a featureId or priceId");if(n.checkEntitlement)return n.checkEntitlement(e);const t=await s.listSubscriptionsByReference(e.referenceId);if(e.priceId!==void 0)return{allowed:_(t,e.priceId),unlimited:!1};if(e.featureId===void 0)throw new f("VALIDATION_ERROR","check() requires a featureId or priceId");if(!o.entitlements)throw new f("CONFIG_INVALID","check() requires `entitlements` to be configured");const r=A(o.entitlements,t);return S(r,t,e.referenceId,e.featureId,e.quantity??1)},createCheckout:async e=>l(e),createPortalSession:async(e,t)=>{await u(e);const r=await s.getCustomerByReference(n.identifier,e);if(!r)throw new f("NOT_FOUND",`no customer for reference "${e}"`);return n.createPortalSession({customerId:r.id,returnUrl:t})},handleWebhook:async e=>{let t;try{const a=await e.text();t=await n.parseWebhook({headers:e.headers,payload:a})}catch(a){if(a instanceof f){const{body:i,status:d}=N(a);return y({error:i.message},d)}return y({error:"webhook error"},400)}const r=await F(s,t,o.observability);return r.reason==="orphaned"?y({applied:r.applied,reason:r.reason},500):y({applied:r.applied,reason:r.reason},200)},listBalances:async e=>{if(await u(e),n.getBalances)return n.getBalances(e);if(!o.entitlements)throw new f("CONFIG_INVALID","listBalances() requires `entitlements` to be configured");const t=await s.listSubscriptionsByReference(e),r=A(o.entitlements,t),a=U(o.entitlements),i=a.filter(c=>r.limit(c)!==void 0),d=i.length===0?new Map:await s.sumUsageByFeature(e,i,g(t));return a.map(c=>{const m=r.limit(c);return m!==void 0?{featureId:c,...w(m,d.get(c)??0,1)}:{featureId:c,allowed:r.has(c),unlimited:r.has(c)}})},listSubscriptions:async e=>(await u(e),s.listSubscriptionsByReference(e)),refundPayment:async e=>{const t=await p(e.sessionId),r=e.amount??K(t.capturedAmount,t.refundedAmount),a=T(t.refundedAmount,r);if(v(a,t.capturedAmount)>0)throw new f("VALIDATION_ERROR",`refundPayment(): refunding ${String(e.amount?.minorUnits)} would exceed the captured amount on session "${e.sessionId}"`);if(B(r))return t;const i=e.amount??(v(r,t.capturedAmount)===0?void 0:r),d=e.idempotencyKey??await b("refund_payment",n.identifier,e.sessionId,O(i),e.reason??""),c=await n.refundPayment({...e,amount:i,idempotencyKey:d});if(c.pending)return t;const m=D(e.sessionId,c.refundId,r);if(!await s.markEventProcessed(n.identifier,m,E)&&c.refundId!==void 0)return t;try{return await I(t,{refundedAmount:a,state:v(a,t.capturedAmount)<0?"partially_refunded":"refunded"})}catch(P){throw await s.releaseEvent(n.identifier,m),P}},store:s,track:async e=>{await u(e.referenceId);const t=e.quantity===void 0?1:e.quantity;if(!Number.isSafeInteger(t)||t<0)throw new f("VALIDATION_ERROR",`track(): \`quantity\` must be a non-negative safe integer (got ${String(e.quantity)})`);const r=e.idempotencyKey??crypto.randomUUID(),a=e.mode==="set";if(a&&n.capabilities.usageMetering&&n.reportUsage)throw new f("VALIDATION_ERROR",`track(): \`mode: "set"\` is not supported on "${n.identifier}", whose meter is additive — a lowering set cannot be forwarded, so the provider would over-bill against the local period total. Use \`mode: "add"\`.`);const i=a?await s.listSubscriptionsByReference(e.referenceId):void 0,d=i===void 0?0:await s.sumUsage(e.referenceId,e.featureId,g(i));if(a?t===d:t===0)return{recorded:!1,reportedToProvider:!1};const c=t;if(!await s.recordUsage({createdAt:$(),featureId:e.featureId,idempotencyKey:r,...a?{mode:"set"}:{},provider:n.identifier,quantity:t,referenceId:e.referenceId,reportedToProvider:!1}))return{recorded:!1,reportedToProvider:!1};if(c<=0||!n.capabilities.usageMetering||!n.reportUsage)return{recorded:!0,reportedToProvider:!1};try{const R=await s.getCustomerByReference(n.identifier,e.referenceId);return await n.reportUsage({customerId:R?.id,featureId:e.featureId,idempotencyKey:r,quantity:c,referenceId:e.referenceId}),await s.markUsageReported(n.identifier,r),{recorded:!0,reportedToProvider:!0}}catch{return C(o.observability,{featureId:e.featureId,provider:n.identifier,referenceId:e.referenceId,type:"usage.report_failed"}),{recorded:!0,reportedToProvider:!1}}}}};export{H as createPayment,G as webhookResponse};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const f=new Set(["active","trialing"]),u=s=>s.priceIds??[s.priceId],p=s=>{let t=0;for(const e of s)f.has(e.state)&&e.currentPeriodStart!==void 0&&(t=Math.max(t,e.currentPeriodStart));return t},S=s=>{const t=new Set;for(const e of Object.values(s.plans)){for(const a of e.features??[])t.add(a);for(const a of Object.keys(e.limits??{}))t.add(a)}return[...t].toSorted((e,a)=>e.localeCompare(a))},h=(s,t)=>s.some(e=>f.has(e.state)&&u(e).includes(t)),m=(s,t)=>{const e=new Set(t.filter(n=>f.has(n.state)).flatMap(n=>u(n))),a=[],r=new Set,c=new Map;for(const[n,i]of Object.entries(s.plans))if(i.priceIds.some(o=>e.has(o))){a.push(n);for(const o of i.features??[])r.add(o);for(const[o,l]of Object.entries(i.limits??{})){const d=c.get(o);c.set(o,d===void 0?l:Math.max(d,l))}}return{features:r,has:n=>r.has(n),limit:n=>c.get(n),plans:a}},v=async(s,t,e)=>m(t,await s.listSubscriptionsByReference(e));export{v as entitlementsForReference,S as featureNames,h as hasActivePrice,m as resolveEntitlements,p as usagePeriodStart};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r=new TextEncoder,c=new Set(["dodopayments","polar","stripe"]),a=async n=>[...new Uint8Array(await crypto.subtle.digest("SHA-256",r.encode(n)))].map(t=>t.toString(16).padStart(2,"0")).join(""),s=(n,...t)=>[n,...t.map(String)].join(":"),i=async(n,t,...e)=>{const o=await a(JSON.stringify([t,...e.map(String)]));return`${n}:${t}:${o}`},d=(n,t,e)=>t===void 0?`local-refund:${n}:${e.currency}:${String(e.minorUnits)}`:`local-refund:${n}:id:${t}`,y="marker.local_refund",p=n=>c.has(n);export{y as LOCAL_REFUND_CLAIM_TYPE,i as derivedIdempotencyKey,s as idempotencyKey,d as localRefundKey,p as usageForwardIsIdempotent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n=e=>typeof e=="object"&&e!==null?e:{},a=(e,r)=>typeof e[r]=="string"?e[r]:void 0,s=(e,r)=>typeof e[r]=="number"?e[r]:void 0,d=(e,r)=>typeof e[r]=="boolean"?e[r]:void 0,c=(e,...r)=>{for(const t of r){const o=a(e,t);if(o!==void 0)return o}},i=(e,...r)=>{for(const t of r){const o=s(e,t);if(o!==void 0)return o}},f=e=>a(n(e.metadata),"referenceId"),p=e=>{const r=typeof e=="string"?Date.parse(e):Number.NaN;return Number.isNaN(r)?void 0:r};export{n as a,s as b,c,d,i as e,f,p,a as r};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createPayment as i}from"./createPayment-Dimez2zY.mjs";import{createDatabasePaymentStore as s}from"./createDatabasePaymentStore-BGcHX1uq.mjs";const o=r=>({delete:async t=>r.delete(t),findFirst:async(t,e)=>await r.findFirst(t,{where:e}),findMany:async(t,e,a)=>{const n=await r.findMany(t,{...a,where:e});return{cursor:n.continueCursor??void 0,rows:n.page}},insert:async(t,e)=>r.insert(t,e),patch:async(t,e)=>r.patch(t,e)}),y=(r,t)=>{const e=r.auth?.userId??void 0;return i({adapter:t.adapter,authorize:t.authorize??(a=>a.trim()!==""&&e!==void 0&&a===e),entitlements:t.entitlements,observability:t.observability,store:s(o(r.db))})};export{o as lunoraDatabaseToPaymentDatabase,y as paymentsFromContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraPaymentError as t}from"./LunoraPaymentError-BSxyhWgu.mjs";const p=o=>r=>{throw new t("PROVIDER_ERROR",`${o} does not support ${r}`)};export{p as m};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r=(t,n)=>{if(t)try{t(n)}catch{}};export{r as n};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineTable as r}from"@lunora/server";import{v as e}from"@lunora/values";const n=r({createdAt:e.number(),email:e.optional(e.string()),provider:e.string(),providerCustomerId:e.string(),referenceId:e.string()}).index("by_provider_customer",["provider","providerCustomerId"],{unique:!0}).index("by_reference",["referenceId"]),i=r({cancelAtPeriodEnd:e.boolean(),createdAt:e.number(),currentPeriodEnd:e.optional(e.number()),currentPeriodStart:e.optional(e.number()),priceId:e.string(),priceIds:e.optional(e.array(e.string())),provider:e.string(),providerSubscriptionId:e.string(),quantity:e.number(),referenceId:e.string(),state:e.string(),updatedAt:e.number()}).index("by_provider_subscription",["provider","providerSubscriptionId"],{unique:!0}).index("by_reference",["referenceId"]),t=r({amountMinor:e.bigint(),capturedMinor:e.bigint(),createdAt:e.number(),currency:e.string(),provider:e.string(),providerSessionId:e.string(),referenceId:e.string(),refundedMinor:e.bigint(),state:e.string(),updatedAt:e.number()}).index("by_provider_session",["provider","providerSessionId"],{unique:!0}).index("by_reference",["referenceId"]),d=r({processedAt:e.number(),provider:e.string(),providerEventId:e.string(),type:e.string()}).index("by_provider_event",["provider","providerEventId"],{unique:!0}),o=r({createdAt:e.number(),featureId:e.string(),idempotencyKey:e.string(),mode:e.optional(e.string()),provider:e.string(),quantity:e.number(),referenceId:e.string(),reportedToProvider:e.boolean()}).index("by_idempotency",["provider","idempotencyKey"],{unique:!0}).index("by_reference_feature",["referenceId","featureId"]),p={customers:n,events:d,paymentSessions:t,subscriptions:i,usageEvents:o};export{p as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{usageForwardIsIdempotent as l}from"./idempotencyKey-qQJLcEQX.mjs";import{compareMoney as y}from"./addMoney-B4ufWAnD.mjs";import{n as u}from"./observability-BteZ2dJS.mjs";const I=100,f=(e,t)=>e.currency===t.currency&&y(e,t)===0,A=(e,t)=>e?.state!==t.state||e.cancelAtPeriodEnd!==t.cancelAtPeriodEnd||e.currentPeriodEnd!==t.currentPeriodEnd||e.priceId!==t.priceId||e.quantity!==t.quantity,b=(e,t)=>e?.state!==t.state||!f(e.capturedAmount,t.capturedAmount)||!f(e.refundedAmount,t.refundedAmount),p={captured:0,partially_refunded:1,refunded:2},w=(e,t)=>{if(!e)return t;const a=e.refundedAmount.currency===t.refundedAmount.currency&&y(e.refundedAmount,t.refundedAmount)>0?e.refundedAmount:t.refundedAmount,d=p[e.state],r=p[t.state],i=d!==void 0&&r!==void 0&&r<d?e.state:t.state,o=t.referenceId===""?e.referenceId:t.referenceId;return{...t,referenceId:o,refundedAmount:a,state:i}},S=(e,t)=>e===void 0||e===""?t:e,P=async(e,t,n,a)=>{const d=await e.getSubscriptionStatus(n),r=await t.getSubscription(e.identifier,n);return A(r,d)?(await t.upsertSubscription({...d,createdAt:r?.createdAt??d.createdAt,referenceId:S(r?.referenceId,d.referenceId)}),u(a,{id:n,kind:"subscription",provider:e.identifier,type:"reconcile.drift"}),!0):!1},v=async(e,t,n,a)=>{const d=await e.getPaymentStatus(n),r=await t.getPaymentSession(e.identifier,n),i=w(r,d);return b(r,i)?(await t.upsertPaymentSession({...i,createdAt:r?.createdAt??i.createdAt}),u(a,{id:n,kind:"payment",provider:e.identifier,type:"reconcile.drift"}),!0):!1},g=async(e,t,n,a)=>{const{reportUsage:d}=e;if(n<=0||d===void 0||!e.capabilities.usageMetering||!l(e.identifier))return{checked:0,failed:0,updated:0};const r=await t.listUnreportedUsage(e.identifier,n);let i=0,o=0;for(const c of r)try{const s=await t.getCustomerByReference(e.identifier,c.referenceId);await d({customerId:s?.id,featureId:c.featureId,idempotencyKey:c.idempotencyKey,quantity:c.quantity,referenceId:c.referenceId}),await t.markUsageReported(e.identifier,c.idempotencyKey),i+=1}catch{o+=1,u(a,{featureId:c.featureId,provider:e.identifier,referenceId:c.referenceId,type:"usage.report_failed"})}return{checked:r.length,failed:o,updated:i}},m=async(e,t,n,a,d)=>{const r=await Promise.allSettled(e.map(c=>n(c)));let i=0,o=0;for(const[c,s]of r.entries())s.status==="fulfilled"?s.value&&(i+=1):(o+=1,u(d,{error:s.reason,id:e[c]??"",kind:t,provider:a.identifier,type:"reconcile.error"}));return{failed:o,updated:i}},U=async e=>{const{adapter:t,observability:n,store:a}=e,d=e.subscriptionIds??[],r=e.paymentSessionIds??[],i=await m(d,"subscription",s=>P(t,a,s,n),t,n),o=await m(r,"payment",s=>v(t,a,s,n),t,n),c=await g(t,a,e.usageReportLimit??I,n);return u(n,{failedPayments:o.failed,failedSubscriptions:i.failed,provider:t.identifier,type:"reconcile.completed",updatedPayments:o.updated,updatedSubscriptions:i.updated}),{checkedPayments:r.length,checkedSubscriptions:d.length,checkedUsage:c.checked,failedPayments:o.failed,failedSubscriptions:i.failed,failedUsage:c.failed,updatedPayments:o.updated,updatedSubscriptions:i.updated,updatedUsage:c.updated}};export{U as reconcile};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i={active:"subscription.active",canceled:"subscription.canceled",past_due:"subscription.past_due",paused:"subscription.paused",trialing:"subscription.active"},t=s=>s===void 0?"subscription.updated":i[s]??"subscription.updated";export{t as s};
|