@lunora/payment 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,190 @@
1
+ import { usagePeriodStart, resolveEntitlements, featureNames, hasActivePrice } from './entitlementsForReference-CzZGXPoZ.mjs';
2
+ import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
3
+ import idempotencyKey from './idempotencyKey-BFzDCA7g.mjs';
4
+ import { n as notifyObserver } from './observability-CvhJ205g.mjs';
5
+ import applyWebhookAction from './applyWebhookAction-DpAqf3Lw.mjs';
6
+
7
+ const jsonResponse = (body, status) => Response.json(body, { headers: { "content-type": "application/json" }, status });
8
+ const stripReferenceId = (metadata) => metadata && "referenceId" in metadata ? Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "referenceId")) : metadata;
9
+ const createPayment = (options) => {
10
+ const { adapter, store } = options;
11
+ const ensureAuthorized = async (referenceId) => {
12
+ if (!options.authorize) {
13
+ return;
14
+ }
15
+ let allowed;
16
+ try {
17
+ allowed = await options.authorize(referenceId);
18
+ } catch {
19
+ throw new LunoraPaymentError("FORBIDDEN", `caller not authorized for reference "${referenceId}"`);
20
+ }
21
+ if (!allowed) {
22
+ throw new LunoraPaymentError("FORBIDDEN", `caller not authorized for reference "${referenceId}"`);
23
+ }
24
+ };
25
+ const startCheckout = async (input) => {
26
+ await ensureAuthorized(input.referenceId);
27
+ const metadata = stripReferenceId(input.metadata);
28
+ let { customerId } = input;
29
+ if (!customerId) {
30
+ const existing = await store.getCustomerByReference(adapter.identifier, input.referenceId);
31
+ if (existing) {
32
+ customerId = existing.id;
33
+ } else {
34
+ const customer = await adapter.getOrCreateCustomer({ referenceId: input.referenceId });
35
+ customerId = customer.id;
36
+ await store.upsertCustomer(customer);
37
+ }
38
+ }
39
+ const key = input.idempotencyKey ?? idempotencyKey(
40
+ "checkout",
41
+ adapter.identifier,
42
+ input.referenceId,
43
+ input.priceId,
44
+ input.mode,
45
+ String(input.quantity ?? 1),
46
+ input.successUrl,
47
+ input.cancelUrl,
48
+ metadata ? JSON.stringify(metadata) : ""
49
+ );
50
+ return adapter.createCheckout({ ...input, customerId, idempotencyKey: key, metadata });
51
+ };
52
+ const evaluateFeature = async (entitlements, subscriptions, referenceId, featureId, need) => {
53
+ const limit = entitlements.limit(featureId);
54
+ if (limit !== void 0) {
55
+ const used = await store.sumUsage(referenceId, featureId, usagePeriodStart(subscriptions));
56
+ const balance = limit - used;
57
+ return { allowed: balance >= need, balance, limit, unlimited: false, used };
58
+ }
59
+ return { allowed: entitlements.has(featureId), unlimited: entitlements.has(featureId) };
60
+ };
61
+ return {
62
+ adapter,
63
+ attach: async (input) => startCheckout({ ...input, mode: input.mode ?? "subscription" }),
64
+ cancelSubscription: async (subscriptionId, cancelOptions) => {
65
+ const existing = await store.getSubscription(adapter.identifier, subscriptionId);
66
+ if (!existing) {
67
+ throw new LunoraPaymentError("NOT_FOUND", `subscription "${subscriptionId}" not found`);
68
+ }
69
+ try {
70
+ await ensureAuthorized(existing.referenceId);
71
+ } catch {
72
+ throw new LunoraPaymentError("NOT_FOUND", `subscription "${subscriptionId}" not found`);
73
+ }
74
+ const key = cancelOptions?.idempotencyKey ?? idempotencyKey("cancel_subscription", adapter.identifier, subscriptionId);
75
+ const updated = await adapter.cancelSubscription(subscriptionId, { ...cancelOptions, idempotencyKey: key });
76
+ await store.upsertSubscription(updated);
77
+ return updated;
78
+ },
79
+ check: async (input) => {
80
+ await ensureAuthorized(input.referenceId);
81
+ const subscriptions = await store.listSubscriptionsByReference(input.referenceId);
82
+ if (input.priceId !== void 0) {
83
+ return { allowed: hasActivePrice(subscriptions, input.priceId), unlimited: false };
84
+ }
85
+ if (input.featureId === void 0) {
86
+ throw new LunoraPaymentError("CONFIG_INVALID", "check() requires a featureId or priceId");
87
+ }
88
+ if (!options.entitlements) {
89
+ throw new LunoraPaymentError("CONFIG_INVALID", "check() requires `entitlements` to be configured");
90
+ }
91
+ const entitlements = resolveEntitlements(options.entitlements, subscriptions);
92
+ return evaluateFeature(entitlements, subscriptions, input.referenceId, input.featureId, input.quantity ?? 1);
93
+ },
94
+ createCheckout: async (input) => startCheckout(input),
95
+ createPortalSession: async (referenceId, returnUrl) => {
96
+ await ensureAuthorized(referenceId);
97
+ const customer = await store.getCustomerByReference(adapter.identifier, referenceId);
98
+ if (!customer) {
99
+ throw new LunoraPaymentError("NOT_FOUND", `no customer for reference "${referenceId}"`);
100
+ }
101
+ return adapter.createPortalSession({ customerId: customer.id, returnUrl });
102
+ },
103
+ handleWebhook: async (request) => {
104
+ let action;
105
+ try {
106
+ const payload = await request.text();
107
+ action = await adapter.parseWebhook({ headers: request.headers, payload });
108
+ } catch (error) {
109
+ if (error instanceof LunoraPaymentError) {
110
+ return jsonResponse({ error: error.message }, error.status);
111
+ }
112
+ return jsonResponse({ error: "webhook error" }, 400);
113
+ }
114
+ const result = await applyWebhookAction(store, action, options.observability);
115
+ return jsonResponse({ applied: result.applied, reason: result.reason }, 200);
116
+ },
117
+ listBalances: async (referenceId) => {
118
+ await ensureAuthorized(referenceId);
119
+ if (!options.entitlements) {
120
+ throw new LunoraPaymentError("CONFIG_INVALID", "listBalances() requires `entitlements` to be configured");
121
+ }
122
+ const subscriptions = await store.listSubscriptionsByReference(referenceId);
123
+ const entitlements = resolveEntitlements(options.entitlements, subscriptions);
124
+ return Promise.all(
125
+ featureNames(options.entitlements).map(async (featureId) => {
126
+ return {
127
+ featureId,
128
+ ...await evaluateFeature(entitlements, subscriptions, referenceId, featureId, 1)
129
+ };
130
+ })
131
+ );
132
+ },
133
+ listSubscriptions: async (referenceId) => {
134
+ await ensureAuthorized(referenceId);
135
+ return store.listSubscriptionsByReference(referenceId);
136
+ },
137
+ store,
138
+ track: async (input) => {
139
+ await ensureAuthorized(input.referenceId);
140
+ const target = input.quantity ?? 1;
141
+ const key = input.idempotencyKey ?? crypto.randomUUID();
142
+ let delta = target;
143
+ if (input.mode === "set") {
144
+ const subscriptions = await store.listSubscriptionsByReference(input.referenceId);
145
+ const current = await store.sumUsage(input.referenceId, input.featureId, usagePeriodStart(subscriptions));
146
+ delta = target - current;
147
+ }
148
+ if (delta === 0) {
149
+ return { recorded: false, reportedToProvider: false };
150
+ }
151
+ const recorded = await store.recordUsage({
152
+ createdAt: Date.now(),
153
+ featureId: input.featureId,
154
+ idempotencyKey: key,
155
+ provider: adapter.identifier,
156
+ quantity: delta,
157
+ referenceId: input.referenceId,
158
+ reportedToProvider: false
159
+ });
160
+ if (!recorded) {
161
+ return { recorded: false, reportedToProvider: false };
162
+ }
163
+ if (delta <= 0 || !adapter.capabilities.usageMetering || !adapter.reportUsage) {
164
+ return { recorded: true, reportedToProvider: false };
165
+ }
166
+ try {
167
+ const customer = await store.getCustomerByReference(adapter.identifier, input.referenceId);
168
+ await adapter.reportUsage({
169
+ customerId: customer?.id,
170
+ featureId: input.featureId,
171
+ idempotencyKey: key,
172
+ quantity: delta,
173
+ referenceId: input.referenceId
174
+ });
175
+ await store.markUsageReported(adapter.identifier, key);
176
+ return { recorded: true, reportedToProvider: true };
177
+ } catch {
178
+ notifyObserver(options.observability, {
179
+ featureId: input.featureId,
180
+ provider: adapter.identifier,
181
+ referenceId: input.referenceId,
182
+ type: "usage.report_failed"
183
+ });
184
+ return { recorded: true, reportedToProvider: false };
185
+ }
186
+ }
187
+ };
188
+ };
189
+
190
+ export { createPayment };
@@ -0,0 +1,217 @@
1
+ import { LunoraPaymentError } from './LunoraPaymentError-B3hEzXSs.mjs';
2
+ import { a as asRecord, r as readString, b as readBoolean, c as readNumber } from './json-Db337f36.mjs';
3
+ import { money, zeroMoney } from './addMoney-bCcs1nyw.mjs';
4
+ import { verifyStandardWebhook } from './constantTimeEqual-CfY0jYcL.mjs';
5
+
6
+ const PAYMENT_STATE_BY_POLAR_ORDER_STATUS = {
7
+ paid: "captured",
8
+ partially_refunded: "partially_refunded",
9
+ pending: "initiated",
10
+ refunded: "refunded"
11
+ };
12
+ const SUBSCRIPTION_STATE_BY_POLAR_STATUS = {
13
+ active: "active",
14
+ canceled: "canceled",
15
+ incomplete: "trialing",
16
+ incomplete_expired: "canceled",
17
+ past_due: "past_due",
18
+ trialing: "trialing",
19
+ unpaid: "past_due"
20
+ };
21
+ const notSupported = (operation) => {
22
+ throw new LunoraPaymentError("PROVIDER_ERROR", `polar (merchant-of-record) does not support ${operation}`);
23
+ };
24
+ const parseTimestamp = (value) => {
25
+ const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
26
+ return Number.isNaN(parsed) ? void 0 : parsed;
27
+ };
28
+ const subscriptionFromPolar = (subscription) => {
29
+ const now = Date.now();
30
+ return {
31
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd ?? false,
32
+ createdAt: now,
33
+ currentPeriodEnd: parseTimestamp(subscription.currentPeriodEnd),
34
+ currentPeriodStart: parseTimestamp(subscription.currentPeriodStart),
35
+ id: subscription.id,
36
+ priceId: subscription.productId ?? "",
37
+ provider: "polar",
38
+ quantity: 1,
39
+ referenceId: subscription.metadata?.referenceId ?? "",
40
+ state: SUBSCRIPTION_STATE_BY_POLAR_STATUS[subscription.status] ?? "active",
41
+ updatedAt: now
42
+ };
43
+ };
44
+ const orderToSession = (order) => {
45
+ const now = Date.now();
46
+ const currency = order.currency ?? "usd";
47
+ const amount = money(BigInt(order.totalAmount ?? order.amount ?? 0), currency);
48
+ const state = PAYMENT_STATE_BY_POLAR_ORDER_STATUS[order.status] ?? "initiated";
49
+ const settled = state === "captured" || state === "partially_refunded" || state === "refunded";
50
+ return {
51
+ amount,
52
+ capturedAmount: settled ? amount : zeroMoney(currency),
53
+ createdAt: now,
54
+ id: order.id,
55
+ provider: "polar",
56
+ referenceId: "",
57
+ refundedAmount: state === "refunded" ? amount : zeroMoney(currency),
58
+ state,
59
+ updatedAt: now
60
+ };
61
+ };
62
+ const subscriptionEventType = (status) => {
63
+ const state = status ? SUBSCRIPTION_STATE_BY_POLAR_STATUS[status] : void 0;
64
+ if (state === "canceled") {
65
+ return "subscription.canceled";
66
+ }
67
+ if (state === "past_due") {
68
+ return "subscription.past_due";
69
+ }
70
+ if (state === "active" || state === "trialing") {
71
+ return "subscription.active";
72
+ }
73
+ return "subscription.updated";
74
+ };
75
+ const referenceFromMetadata = (object) => readString(asRecord(object.metadata), "referenceId");
76
+ const mapEvent = (eventId, eventType, object) => {
77
+ const base = { eventId, provider: "polar", raw: { object, type: eventType } };
78
+ const currency = readString(object, "currency") ?? "usd";
79
+ switch (eventType) {
80
+ case "order.created":
81
+ case "order.paid": {
82
+ return {
83
+ ...base,
84
+ amount: money(BigInt(readNumber(object, "total_amount") ?? readNumber(object, "amount") ?? 0), currency),
85
+ customerId: readString(object, "customer_id"),
86
+ referenceId: referenceFromMetadata(object),
87
+ sessionId: readString(object, "id"),
88
+ subscriptionId: readString(object, "subscription_id"),
89
+ type: "payment.captured"
90
+ };
91
+ }
92
+ case "refund.created": {
93
+ return {
94
+ ...base,
95
+ amount: money(BigInt(readNumber(object, "amount") ?? 0), currency),
96
+ referenceId: referenceFromMetadata(object),
97
+ sessionId: readString(object, "order_id") ?? readString(object, "id"),
98
+ type: "payment.refunded"
99
+ };
100
+ }
101
+ case "subscription.active":
102
+ case "subscription.canceled":
103
+ case "subscription.created":
104
+ case "subscription.revoked":
105
+ case "subscription.updated": {
106
+ const type = eventType === "subscription.revoked" ? "subscription.canceled" : subscriptionEventType(readString(object, "status"));
107
+ return {
108
+ ...base,
109
+ cancelAtPeriodEnd: readBoolean(object, "cancel_at_period_end"),
110
+ currentPeriodEnd: parseTimestamp(readString(object, "current_period_end")),
111
+ currentPeriodStart: parseTimestamp(readString(object, "current_period_start")),
112
+ customerId: readString(object, "customer_id"),
113
+ priceId: readString(object, "product_id"),
114
+ referenceId: referenceFromMetadata(object),
115
+ subscriptionId: readString(object, "id"),
116
+ type
117
+ };
118
+ }
119
+ default: {
120
+ return { ...base, type: "unhandled" };
121
+ }
122
+ }
123
+ };
124
+ const createPolarAdapter = (options) => {
125
+ const { client, webhookSecret } = options;
126
+ return {
127
+ cancelPayment: () => notSupported("manual payment cancellation"),
128
+ cancelSubscription: async (subscriptionId, cancelOptions) => {
129
+ const subscription = cancelOptions?.atPeriodEnd ? await client.subscriptions.update({ id: subscriptionId, subscriptionUpdate: { cancelAtPeriodEnd: true } }) : await client.subscriptions.revoke({ id: subscriptionId });
130
+ return subscriptionFromPolar(subscription);
131
+ },
132
+ capabilities: { merchantOfRecord: true, portal: true, usageMetering: true },
133
+ capturePayment: (_input) => notSupported("manual capture"),
134
+ createCheckout: async (input) => {
135
+ const checkout = await client.checkouts.create({
136
+ customerEmail: void 0,
137
+ // Pin the framework-controlled `referenceId` LAST so caller metadata can never override it.
138
+ metadata: { ...input.metadata, referenceId: input.referenceId },
139
+ products: [input.priceId],
140
+ successUrl: input.successUrl
141
+ });
142
+ return { id: checkout.id, provider: "polar", url: checkout.url };
143
+ },
144
+ createPortalSession: async (input) => {
145
+ const session = await client.customerSessions.create({ customerId: input.customerId });
146
+ return { url: session.customerPortalUrl };
147
+ },
148
+ getOrCreateCustomer: async (ref) => {
149
+ const customer = await client.customers.create({
150
+ email: ref.email,
151
+ externalId: ref.referenceId,
152
+ metadata: { ...ref.metadata, referenceId: ref.referenceId }
153
+ });
154
+ return { createdAt: Date.now(), email: customer.email ?? void 0, id: customer.id, provider: "polar", referenceId: ref.referenceId };
155
+ },
156
+ getPaymentStatus: async (sessionId) => orderToSession(await client.orders.get({ id: sessionId })),
157
+ getSubscriptionStatus: async (subscriptionId) => subscriptionFromPolar(await client.subscriptions.get({ id: subscriptionId })),
158
+ identifier: "polar",
159
+ parseWebhook: async ({ headers, payload }) => {
160
+ const webhookId = headers.get("webhook-id") ?? "";
161
+ await verifyStandardWebhook({
162
+ payload,
163
+ secret: webhookSecret,
164
+ toleranceSeconds: options.webhookToleranceSeconds,
165
+ webhookId,
166
+ webhookSignature: headers.get("webhook-signature") ?? "",
167
+ webhookTimestamp: headers.get("webhook-timestamp") ?? ""
168
+ });
169
+ const event = asRecord(JSON.parse(payload));
170
+ return mapEvent(webhookId, readString(event, "type") ?? "", asRecord(event.data));
171
+ },
172
+ refundPayment: async (input) => {
173
+ await client.refunds.create({
174
+ amount: input.amount ? Number(input.amount.minorUnits) : void 0,
175
+ orderId: input.sessionId,
176
+ reason: input.reason ?? "customer_request"
177
+ });
178
+ const refundedAmount = input.amount ?? money(0, "usd");
179
+ return {
180
+ amount: refundedAmount,
181
+ capturedAmount: refundedAmount,
182
+ createdAt: Date.now(),
183
+ id: input.sessionId,
184
+ provider: "polar",
185
+ referenceId: "",
186
+ refundedAmount,
187
+ state: "refunded",
188
+ updatedAt: Date.now()
189
+ };
190
+ },
191
+ reportUsage: async (input) => {
192
+ await client.events.ingest({
193
+ events: [
194
+ {
195
+ externalCustomerId: input.referenceId,
196
+ metadata: { value: input.quantity },
197
+ name: input.featureId,
198
+ timestamp: input.timestamp === void 0 ? void 0 : new Date(input.timestamp).toISOString()
199
+ }
200
+ ]
201
+ });
202
+ },
203
+ resumeSubscription: async (subscriptionId) => {
204
+ const subscription = await client.subscriptions.update({ id: subscriptionId, subscriptionUpdate: { cancelAtPeriodEnd: false } });
205
+ return subscriptionFromPolar(subscription);
206
+ },
207
+ updateSubscription: async (subscriptionId, patch) => {
208
+ const subscription = await client.subscriptions.update({
209
+ id: subscriptionId,
210
+ subscriptionUpdate: patch.priceId ? { productId: patch.priceId } : {}
211
+ });
212
+ return subscriptionFromPolar(subscription);
213
+ }
214
+ };
215
+ };
216
+
217
+ export { createPolarAdapter };
@@ -0,0 +1,270 @@
1
+ import idempotencyKey from './idempotencyKey-BFzDCA7g.mjs';
2
+ import { a as asRecord, r as readString, c as readNumber, b as readBoolean } from './json-Db337f36.mjs';
3
+ import { compareMoney, money, zeroMoney } from './addMoney-bCcs1nyw.mjs';
4
+ import { verifyStripeSignature } from './constantTimeEqual-CfY0jYcL.mjs';
5
+
6
+ const PAYMENT_STATE_BY_STRIPE_STATUS = {
7
+ canceled: "canceled",
8
+ processing: "authorized",
9
+ requires_action: "authorized",
10
+ requires_capture: "authorized",
11
+ requires_confirmation: "initiated",
12
+ requires_payment_method: "initiated",
13
+ succeeded: "captured"
14
+ };
15
+ const SUBSCRIPTION_STATE_BY_STRIPE_STATUS = {
16
+ active: "active",
17
+ canceled: "canceled",
18
+ incomplete: "trialing",
19
+ incomplete_expired: "canceled",
20
+ past_due: "past_due",
21
+ paused: "paused",
22
+ trialing: "trialing",
23
+ unpaid: "past_due"
24
+ };
25
+ const readReferenceId = (object) => readString(asRecord(object.metadata), "referenceId") ?? readString(object, "client_reference_id");
26
+ const firstItem = (object) => {
27
+ const items = asRecord(object.items);
28
+ const data = Array.isArray(items.data) ? items.data : [];
29
+ return asRecord(data[0]);
30
+ };
31
+ const firstPriceId = (object) => readString(asRecord(firstItem(object).price), "id");
32
+ const firstQuantity = (object) => readNumber(firstItem(object), "quantity");
33
+ const periodEndMs = (object) => {
34
+ const seconds = readNumber(object, "current_period_end");
35
+ return seconds === void 0 ? void 0 : seconds * 1e3;
36
+ };
37
+ const periodStartMs = (object) => {
38
+ const seconds = readNumber(object, "current_period_start");
39
+ return seconds === void 0 ? void 0 : seconds * 1e3;
40
+ };
41
+ const intentToSession = (intent) => {
42
+ const amount = money(BigInt(intent.amount), intent.currency);
43
+ const state = PAYMENT_STATE_BY_STRIPE_STATUS[intent.status] ?? "initiated";
44
+ const now = Date.now();
45
+ return {
46
+ amount,
47
+ capturedAmount: state === "captured" ? money(BigInt(intent.amount_received ?? intent.amount), intent.currency) : zeroMoney(intent.currency),
48
+ createdAt: now,
49
+ id: intent.id,
50
+ provider: "stripe",
51
+ referenceId: intent.metadata?.referenceId ?? "",
52
+ refundedAmount: zeroMoney(intent.currency),
53
+ state,
54
+ updatedAt: now
55
+ };
56
+ };
57
+ const subscriptionFromStripe = (subscription) => {
58
+ const now = Date.now();
59
+ return {
60
+ cancelAtPeriodEnd: subscription.cancel_at_period_end ?? false,
61
+ createdAt: now,
62
+ currentPeriodEnd: subscription.current_period_end ? subscription.current_period_end * 1e3 : void 0,
63
+ currentPeriodStart: subscription.current_period_start ? subscription.current_period_start * 1e3 : void 0,
64
+ id: subscription.id,
65
+ priceId: subscription.items?.data[0]?.price?.id ?? "",
66
+ provider: "stripe",
67
+ quantity: subscription.items?.data[0]?.quantity ?? 1,
68
+ referenceId: subscription.metadata?.referenceId ?? "",
69
+ state: SUBSCRIPTION_STATE_BY_STRIPE_STATUS[subscription.status] ?? "active",
70
+ updatedAt: now
71
+ };
72
+ };
73
+ const subscriptionEventType = (status) => {
74
+ const state = status ? SUBSCRIPTION_STATE_BY_STRIPE_STATUS[status] : void 0;
75
+ if (state === "canceled") {
76
+ return "subscription.canceled";
77
+ }
78
+ if (state === "past_due") {
79
+ return "subscription.past_due";
80
+ }
81
+ if (state === "paused") {
82
+ return "subscription.paused";
83
+ }
84
+ if (state === "active" || state === "trialing") {
85
+ return "subscription.active";
86
+ }
87
+ return "subscription.updated";
88
+ };
89
+ const mapEvent = (eventId, eventType, object) => {
90
+ const base = { eventId, provider: "stripe", raw: { object, type: eventType } };
91
+ const currency = readString(object, "currency") ?? "usd";
92
+ switch (eventType) {
93
+ case "charge.refunded": {
94
+ return {
95
+ ...base,
96
+ amount: money(BigInt(readNumber(object, "amount_refunded") ?? 0), currency),
97
+ amountKind: "absolute",
98
+ referenceId: readReferenceId(object),
99
+ sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
100
+ type: "payment.refunded"
101
+ };
102
+ }
103
+ case "checkout.session.completed": {
104
+ if (readString(object, "mode") === "subscription") {
105
+ return {
106
+ ...base,
107
+ customerId: readString(object, "customer"),
108
+ referenceId: readReferenceId(object),
109
+ subscriptionId: readString(object, "subscription"),
110
+ type: "subscription.active"
111
+ };
112
+ }
113
+ const amountTotal = readNumber(object, "amount_total");
114
+ return {
115
+ ...base,
116
+ amount: amountTotal === void 0 ? void 0 : money(BigInt(amountTotal), currency),
117
+ customerId: readString(object, "customer"),
118
+ referenceId: readReferenceId(object),
119
+ sessionId: readString(object, "payment_intent") ?? readString(object, "id"),
120
+ type: "payment.captured"
121
+ };
122
+ }
123
+ case "customer.subscription.created":
124
+ case "customer.subscription.updated": {
125
+ return {
126
+ ...base,
127
+ cancelAtPeriodEnd: readBoolean(object, "cancel_at_period_end"),
128
+ currentPeriodEnd: periodEndMs(object),
129
+ currentPeriodStart: periodStartMs(object),
130
+ customerId: readString(object, "customer"),
131
+ priceId: firstPriceId(object),
132
+ quantity: firstQuantity(object),
133
+ referenceId: readReferenceId(object),
134
+ subscriptionId: readString(object, "id"),
135
+ type: subscriptionEventType(readString(object, "status"))
136
+ };
137
+ }
138
+ case "customer.subscription.deleted": {
139
+ return { ...base, referenceId: readReferenceId(object), subscriptionId: readString(object, "id"), type: "subscription.canceled" };
140
+ }
141
+ case "payment_intent.amount_capturable_updated": {
142
+ return {
143
+ ...base,
144
+ amount: money(BigInt(readNumber(object, "amount") ?? 0), currency),
145
+ referenceId: readReferenceId(object),
146
+ sessionId: readString(object, "id"),
147
+ type: "payment.authorized"
148
+ };
149
+ }
150
+ case "payment_intent.payment_failed": {
151
+ return { ...base, referenceId: readReferenceId(object), sessionId: readString(object, "id"), type: "payment.failed" };
152
+ }
153
+ case "payment_intent.succeeded": {
154
+ return {
155
+ ...base,
156
+ amount: money(BigInt(readNumber(object, "amount_received") ?? readNumber(object, "amount") ?? 0), currency),
157
+ customerId: readString(object, "customer"),
158
+ referenceId: readReferenceId(object),
159
+ sessionId: readString(object, "id"),
160
+ type: "payment.captured"
161
+ };
162
+ }
163
+ default: {
164
+ return { ...base, type: "unhandled" };
165
+ }
166
+ }
167
+ };
168
+ const createStripeAdapter = (options) => {
169
+ const { client, webhookSecret } = options;
170
+ return {
171
+ cancelPayment: async (sessionId, paymentOptions) => {
172
+ const intent = await client.paymentIntents.cancel(sessionId, void 0, { idempotencyKey: paymentOptions?.idempotencyKey });
173
+ return intentToSession(intent);
174
+ },
175
+ cancelSubscription: async (subscriptionId, cancelOptions) => {
176
+ const subscription = cancelOptions?.atPeriodEnd ? await client.subscriptions.update(subscriptionId, { cancel_at_period_end: true }, { idempotencyKey: cancelOptions.idempotencyKey }) : await client.subscriptions.cancel(subscriptionId, void 0, { idempotencyKey: cancelOptions?.idempotencyKey });
177
+ return subscriptionFromStripe(subscription);
178
+ },
179
+ capabilities: { merchantOfRecord: false, portal: true, usageMetering: true },
180
+ capturePayment: async (input) => {
181
+ const intent = await client.paymentIntents.capture(
182
+ input.sessionId,
183
+ input.amount ? { amount_to_capture: Number(input.amount.minorUnits) } : void 0,
184
+ { idempotencyKey: input.idempotencyKey }
185
+ );
186
+ return intentToSession(intent);
187
+ },
188
+ createCheckout: async (input) => {
189
+ const session = await client.checkout.sessions.create(
190
+ {
191
+ cancel_url: input.cancelUrl,
192
+ client_reference_id: input.referenceId,
193
+ customer: input.customerId,
194
+ line_items: [{ price: input.priceId, quantity: input.quantity ?? 1 }],
195
+ // Pin the framework-controlled `referenceId` LAST so caller metadata can never override it.
196
+ metadata: { ...input.metadata, referenceId: input.referenceId },
197
+ mode: input.mode,
198
+ subscription_data: input.mode === "subscription" ? { metadata: { referenceId: input.referenceId } } : void 0,
199
+ success_url: input.successUrl
200
+ },
201
+ { idempotencyKey: input.idempotencyKey }
202
+ );
203
+ return { id: session.id, provider: "stripe", url: session.url ?? "" };
204
+ },
205
+ createPortalSession: async (input) => {
206
+ const session = await client.billingPortal.sessions.create({ customer: input.customerId, return_url: input.returnUrl });
207
+ return { url: session.url };
208
+ },
209
+ getOrCreateCustomer: async (ref) => {
210
+ const customer = await client.customers.create(
211
+ { email: ref.email, metadata: { ...ref.metadata, referenceId: ref.referenceId } },
212
+ { idempotencyKey: idempotencyKey("customer", "stripe", ref.referenceId) }
213
+ );
214
+ return { createdAt: Date.now(), email: customer.email ?? void 0, id: customer.id, provider: "stripe", referenceId: ref.referenceId };
215
+ },
216
+ getPaymentStatus: async (sessionId) => intentToSession(await client.paymentIntents.retrieve(sessionId)),
217
+ getSubscriptionStatus: async (subscriptionId) => subscriptionFromStripe(await client.subscriptions.retrieve(subscriptionId)),
218
+ identifier: "stripe",
219
+ parseWebhook: async ({ headers, payload }) => {
220
+ const signatureHeader = headers.get("stripe-signature") ?? "";
221
+ await verifyStripeSignature({ payload, secret: webhookSecret, signatureHeader, toleranceSeconds: options.webhookToleranceSeconds });
222
+ const event = asRecord(JSON.parse(payload));
223
+ const object = asRecord(asRecord(event.data).object);
224
+ return mapEvent(readString(event, "id") ?? "", readString(event, "type") ?? "", object);
225
+ },
226
+ refundPayment: async (input) => {
227
+ await client.refunds.create(
228
+ {
229
+ amount: input.amount ? Number(input.amount.minorUnits) : void 0,
230
+ payment_intent: input.sessionId,
231
+ reason: input.reason
232
+ },
233
+ { idempotencyKey: input.idempotencyKey }
234
+ );
235
+ const intent = await client.paymentIntents.retrieve(input.sessionId);
236
+ const session = intentToSession(intent);
237
+ const refundedAmount = input.amount ?? session.capturedAmount;
238
+ const partial = input.amount !== void 0 && compareMoney(input.amount, session.capturedAmount) < 0;
239
+ return { ...session, refundedAmount, state: partial ? "partially_refunded" : "refunded" };
240
+ },
241
+ reportUsage: async (input) => {
242
+ await client.billing.meterEvents.create(
243
+ {
244
+ event_name: input.featureId,
245
+ identifier: input.idempotencyKey,
246
+ payload: { stripe_customer_id: input.customerId, value: String(input.quantity) },
247
+ timestamp: input.timestamp === void 0 ? void 0 : Math.floor(input.timestamp / 1e3)
248
+ },
249
+ { idempotencyKey: input.idempotencyKey }
250
+ );
251
+ },
252
+ resumeSubscription: async (subscriptionId) => {
253
+ const subscription = await client.subscriptions.update(subscriptionId, { cancel_at_period_end: false });
254
+ return subscriptionFromStripe(subscription);
255
+ },
256
+ updateSubscription: async (subscriptionId, patch) => {
257
+ const parameters = {};
258
+ if (patch.quantity !== void 0) {
259
+ parameters.quantity = patch.quantity;
260
+ }
261
+ if (patch.priceId) {
262
+ parameters.items = [{ price: patch.priceId }];
263
+ }
264
+ const subscription = await client.subscriptions.update(subscriptionId, parameters);
265
+ return subscriptionFromStripe(subscription);
266
+ }
267
+ };
268
+ };
269
+
270
+ export { createStripeAdapter };