@stacksjs/payments 0.70.88 → 0.70.91
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/dist/billable/charge.d.ts +9 -0
- package/dist/billable/charge.js +64 -0
- package/dist/billable/checkout.d.ts +6 -0
- package/dist/billable/checkout.js +43 -0
- package/dist/billable/customer.d.ts +15 -0
- package/dist/billable/customer.js +117 -0
- package/dist/billable/index.d.ts +13 -0
- package/dist/billable/index.js +12 -0
- package/dist/billable/intent.d.ts +6 -0
- package/dist/billable/intent.js +18 -0
- package/dist/billable/invoice.d.ts +6 -0
- package/dist/billable/invoice.js +14 -0
- package/dist/billable/payment-method.d.ts +18 -0
- package/dist/billable/payment-method.js +119 -0
- package/dist/billable/price.d.ts +6 -0
- package/dist/billable/price.js +19 -0
- package/dist/billable/product.d.ts +37 -0
- package/dist/billable/product.js +101 -0
- package/dist/billable/setup-products.d.ts +2 -0
- package/dist/billable/setup-products.js +34 -0
- package/dist/billable/subscription.d.ts +12 -0
- package/dist/billable/subscription.js +138 -0
- package/dist/billable/transaction.d.ts +14 -0
- package/dist/billable/transaction.js +25 -0
- package/dist/billable/webhook.d.ts +155 -0
- package/dist/billable/webhook.js +153 -0
- package/dist/drivers/stripe.d.ts +8 -0
- package/dist/drivers/stripe.js +25 -0
- package/dist/idempotency.d.ts +26 -0
- package/dist/idempotency.js +13 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +5 -0
- package/dist/payment.d.ts +182 -0
- package/dist/payment.js +199 -0
- package/dist/stripe.backup.d.ts +1 -0
- package/dist/stripe.backup.js +0 -0
- package/package.json +3 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { saas } from "@stacksjs/config";
|
|
2
|
+
import { err, ok } from "@stacksjs/error-handling";
|
|
3
|
+
import { log } from "@stacksjs/logging";
|
|
4
|
+
import { stripe } from "@stacksjs/payments";
|
|
5
|
+
export async function createStripeProduct() {
|
|
6
|
+
const plans = saas.plans;
|
|
7
|
+
try {
|
|
8
|
+
if (plans !== void 0 && plans.length)
|
|
9
|
+
for (const plan of plans) {
|
|
10
|
+
const product = await stripe.products.create({
|
|
11
|
+
name: plan.productName,
|
|
12
|
+
description: plan.description,
|
|
13
|
+
metadata: plan.metadata
|
|
14
|
+
});
|
|
15
|
+
for (const pricing of plan.pricing)
|
|
16
|
+
if (product) {
|
|
17
|
+
const priceParams = {
|
|
18
|
+
unit_amount: pricing.price,
|
|
19
|
+
currency: pricing.currency,
|
|
20
|
+
product: product.id,
|
|
21
|
+
lookup_key: pricing.key
|
|
22
|
+
};
|
|
23
|
+
if (pricing.interval)
|
|
24
|
+
priceParams.recurring = { interval: pricing.interval };
|
|
25
|
+
await stripe.prices.create(priceParams);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return ok("Migrations generated");
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const e = error instanceof Error ? error : Error(String(error));
|
|
31
|
+
log.error(e);
|
|
32
|
+
return err(e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UserModel } from '@stacksjs/orm';
|
|
2
|
+
import type Stripe from 'stripe';
|
|
3
|
+
export declare const manageSubscription: SubscriptionManager;
|
|
4
|
+
export declare interface SubscriptionManager {
|
|
5
|
+
create: (user: UserModel, type: string, lookupKey: string, params: Partial<Stripe.SubscriptionCreateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
6
|
+
update: (user: UserModel, type: string, lookupKey: string, params: Partial<Stripe.SubscriptionUpdateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
7
|
+
cancel: (subscriptionId: string, params?: Partial<Stripe.SubscriptionCreateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
8
|
+
retrieve: (user: UserModel, subscriptionId: string) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
9
|
+
isValid: (user: UserModel, type: string) => Promise<boolean>
|
|
10
|
+
isIncomplete: (user: UserModel, type: string) => Promise<boolean>
|
|
11
|
+
}
|
|
12
|
+
declare type SubscriptionsTable = Record<string, unknown>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { db } from "@stacksjs/database";
|
|
2
|
+
import { HttpError } from "@stacksjs/error-handling";
|
|
3
|
+
import { isUniqueViolation } from "@stacksjs/orm";
|
|
4
|
+
import { manageCustomer, managePrice, stripe } from "..";
|
|
5
|
+
import { stacksIdempotencyKey } from "../idempotency";
|
|
6
|
+
export const manageSubscription = (() => {
|
|
7
|
+
async function create(user, type, lookupKey, params) {
|
|
8
|
+
const price = await managePrice.retrieveByLookupKey(lookupKey);
|
|
9
|
+
if (!price)
|
|
10
|
+
throw Error("Price does not exist in Stripe");
|
|
11
|
+
const subscriptionItems = [
|
|
12
|
+
{
|
|
13
|
+
price: price.id,
|
|
14
|
+
quantity: 1
|
|
15
|
+
}
|
|
16
|
+
], mergedParams = { ...{
|
|
17
|
+
customer: await manageCustomer.createOrGetStripeUser(user, {}).then((customer) => {
|
|
18
|
+
if (!customer || !customer.id)
|
|
19
|
+
throw Error("Customer does not exist in Stripe");
|
|
20
|
+
return customer.id;
|
|
21
|
+
}),
|
|
22
|
+
payment_behavior: "allow_incomplete",
|
|
23
|
+
expand: ["latest_invoice.payment_intent"],
|
|
24
|
+
items: subscriptionItems
|
|
25
|
+
}, ...params }, subscription = await stripe.subscriptions.create(mergedParams, {
|
|
26
|
+
idempotencyKey: stacksIdempotencyKey("subscription.create", user.id, type, lookupKey)
|
|
27
|
+
});
|
|
28
|
+
await storeSubscription(user, type, lookupKey, subscription);
|
|
29
|
+
return subscription;
|
|
30
|
+
}
|
|
31
|
+
async function update(user, type, lookupKey, _params = {}) {
|
|
32
|
+
const newPrice = await managePrice.retrieveByLookupKey(lookupKey), activeSubscription = await user?.activeSubscription();
|
|
33
|
+
if (!newPrice)
|
|
34
|
+
throw Error("New price does not exist in Stripe");
|
|
35
|
+
if (!activeSubscription)
|
|
36
|
+
throw Error("No active subscription for user!");
|
|
37
|
+
const subscriptionId = activeSubscription.subscription?.provider_id;
|
|
38
|
+
if (!subscriptionId)
|
|
39
|
+
throw Error("Active subscription has no provider ID");
|
|
40
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
41
|
+
if (!subscription)
|
|
42
|
+
throw Error("Subscription does not exist in Stripe");
|
|
43
|
+
const subscriptionItemId = subscription.items.data[0]?.id;
|
|
44
|
+
if (!subscriptionItemId)
|
|
45
|
+
throw Error("No subscription items found in the subscription");
|
|
46
|
+
await stripe.subscriptions.update(subscriptionId, {
|
|
47
|
+
items: [{ id: subscriptionItemId, price: newPrice.id, quantity: 1 }],
|
|
48
|
+
proration_behavior: "create_prorations"
|
|
49
|
+
}, {
|
|
50
|
+
idempotencyKey: stacksIdempotencyKey("subscription.update", subscriptionId, newPrice.id)
|
|
51
|
+
});
|
|
52
|
+
const updatedSubscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
53
|
+
if (!activeSubscription.subscription?.id)
|
|
54
|
+
throw Error("Active subscription has no database ID");
|
|
55
|
+
await updateSubscription(activeSubscription.subscription.id, type, updatedSubscription);
|
|
56
|
+
return updatedSubscription;
|
|
57
|
+
}
|
|
58
|
+
async function cancel(subscriptionId, params) {
|
|
59
|
+
if (!await stripe.subscriptions.retrieve(subscriptionId))
|
|
60
|
+
throw Error("Subscription does not exist or does not belong to the user");
|
|
61
|
+
const updatedSubscription = await stripe.subscriptions.cancel(subscriptionId, params, {
|
|
62
|
+
idempotencyKey: stacksIdempotencyKey("subscription.cancel", subscriptionId)
|
|
63
|
+
});
|
|
64
|
+
await updateStoredSubscription(subscriptionId);
|
|
65
|
+
return updatedSubscription;
|
|
66
|
+
}
|
|
67
|
+
async function retrieve(user, subscriptionId) {
|
|
68
|
+
if (!user.hasStripeId())
|
|
69
|
+
throw Error("Customer does not exist in Stripe");
|
|
70
|
+
return await stripe.subscriptions.retrieve(subscriptionId);
|
|
71
|
+
}
|
|
72
|
+
async function updateStoredSubscription(subscriptionId) {
|
|
73
|
+
await db.updateTable("subscriptions").set({ provider_status: "canceled" }).where("provider_id", "=", subscriptionId).executeTakeFirst();
|
|
74
|
+
}
|
|
75
|
+
function isActive(subscription) {
|
|
76
|
+
return subscription.provider_status === "active";
|
|
77
|
+
}
|
|
78
|
+
function isTrial(subscription) {
|
|
79
|
+
return subscription.provider_status === "trialing";
|
|
80
|
+
}
|
|
81
|
+
async function isIncomplete(user, type) {
|
|
82
|
+
const subscription = await db.selectFrom("subscriptions").where("user_id", "=", user.id).where("type", "=", type).selectAll().executeTakeFirst();
|
|
83
|
+
if (!subscription)
|
|
84
|
+
return !1;
|
|
85
|
+
return subscription.provider_status === "incomplete";
|
|
86
|
+
}
|
|
87
|
+
async function isValid(user, type) {
|
|
88
|
+
const subscription = await db.selectFrom("subscriptions").where("user_id", "=", user.id).where("type", "=", type).selectAll().executeTakeFirst();
|
|
89
|
+
if (!subscription)
|
|
90
|
+
return !1;
|
|
91
|
+
const active = await isActive(subscription), trial = await isTrial(subscription);
|
|
92
|
+
return active || trial;
|
|
93
|
+
}
|
|
94
|
+
async function storeSubscription(user, type, _lookupKey, options) {
|
|
95
|
+
const firstItem = options.items.data[0];
|
|
96
|
+
if (!firstItem)
|
|
97
|
+
throw Error("Stripe subscription contains no line items \u2014 cannot store subscription");
|
|
98
|
+
const data = removeNullValues({
|
|
99
|
+
user_id: user.id,
|
|
100
|
+
type,
|
|
101
|
+
unit_price: Number(firstItem.price.unit_amount),
|
|
102
|
+
provider_id: options.id,
|
|
103
|
+
provider_status: options.status,
|
|
104
|
+
provider_price_id: firstItem.price.id,
|
|
105
|
+
quantity: firstItem.quantity,
|
|
106
|
+
trial_ends_at: options.trial_end != null ? String(options.trial_end) : void 0,
|
|
107
|
+
ends_at: options.current_period_end != null ? String(options.current_period_end) : void 0,
|
|
108
|
+
provider_type: "stripe",
|
|
109
|
+
last_used_at: options.current_period_end != null ? String(options.current_period_end) : void 0
|
|
110
|
+
});
|
|
111
|
+
let subscriptionModelCreated;
|
|
112
|
+
try {
|
|
113
|
+
subscriptionModelCreated = await db.insertInto("subscriptions").values(data).executeTakeFirst();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (isUniqueViolation(error))
|
|
116
|
+
throw new HttpError(409, "A subscription with this provider ID already exists");
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (!subscriptionModelCreated)
|
|
120
|
+
throw Error("Failed to insert subscription record");
|
|
121
|
+
return await db.selectFrom("subscriptions").where("id", "=", Number(subscriptionModelCreated.insertId)).selectAll().executeTakeFirst();
|
|
122
|
+
}
|
|
123
|
+
async function updateSubscription(activeSubId, type, options) {
|
|
124
|
+
const subscription = await db.selectFrom("subscriptions").where("id", "=", activeSubId).selectAll().executeTakeFirst(), firstItem = options.items.data[0];
|
|
125
|
+
if (!firstItem)
|
|
126
|
+
throw Error("Stripe subscription contains no line items \u2014 cannot update subscription");
|
|
127
|
+
await db?.updateTable("subscriptions").set({
|
|
128
|
+
type,
|
|
129
|
+
provider_price_id: firstItem.price.id,
|
|
130
|
+
unit_price: Number(firstItem.price.unit_amount)
|
|
131
|
+
}).where("id", "=", activeSubId).executeTakeFirst();
|
|
132
|
+
return subscription;
|
|
133
|
+
}
|
|
134
|
+
function removeNullValues(obj) {
|
|
135
|
+
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value != null));
|
|
136
|
+
}
|
|
137
|
+
return { create, update, isValid, isIncomplete, cancel, retrieve };
|
|
138
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PaymentTransaction } from '@stacksjs/orm';
|
|
2
|
+
import type { ModelRow, UserModel } from '@stacksjs/orm';
|
|
3
|
+
export declare const manageTransaction: ManageTransaction;
|
|
4
|
+
export declare interface StoreTransactionOptions {
|
|
5
|
+
brand: string
|
|
6
|
+
provider_id: string
|
|
7
|
+
description?: string
|
|
8
|
+
type?: string
|
|
9
|
+
}
|
|
10
|
+
export declare interface ManageTransaction {
|
|
11
|
+
store: (user: UserModel, productId: number, options: StoreTransactionOptions) => Promise<PaymentTransactionsTable | undefined>
|
|
12
|
+
list: (user: UserModel) => Promise<PaymentTransactionsTable[]>
|
|
13
|
+
}
|
|
14
|
+
declare type PaymentTransactionsTable = ModelRow<typeof PaymentTransaction>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { db } from "@stacksjs/database";
|
|
2
|
+
import { PaymentTransaction } from "@stacksjs/orm";
|
|
3
|
+
export const manageTransaction = (() => {
|
|
4
|
+
async function store(user, productId, options) {
|
|
5
|
+
const product = await db.selectFrom("payment_products").where("id", "=", productId).selectAll().executeTakeFirst();
|
|
6
|
+
if (!product)
|
|
7
|
+
throw Error(`Payment product with id ${productId} not found.`);
|
|
8
|
+
const data = {
|
|
9
|
+
name: product.name,
|
|
10
|
+
description: options.description ?? "",
|
|
11
|
+
amount: product.unit_price,
|
|
12
|
+
brand: options.brand,
|
|
13
|
+
type: options.type ?? "one-time",
|
|
14
|
+
provider_id: options.provider_id,
|
|
15
|
+
user_id: user.id
|
|
16
|
+
}, createdTransaction = await db.insertInto("payment_transactions").values(data).executeTakeFirst();
|
|
17
|
+
if (!createdTransaction)
|
|
18
|
+
throw Error("Failed to insert payment transaction");
|
|
19
|
+
return await db.selectFrom("payment_transactions").where("id", "=", Number(createdTransaction.insertId)).selectAll().executeTakeFirst();
|
|
20
|
+
}
|
|
21
|
+
async function list(user) {
|
|
22
|
+
return await db.selectFrom("payment_transactions").where("user_id", "=", user.id).selectAll().execute();
|
|
23
|
+
}
|
|
24
|
+
return { store, list };
|
|
25
|
+
})();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type Stripe from 'stripe';
|
|
2
|
+
/**
|
|
3
|
+
* Register a webhook event handler
|
|
4
|
+
*/
|
|
5
|
+
export declare function onWebhookEvent(eventType: WebhookEventType, handler: WebhookHandler): void;
|
|
6
|
+
/**
|
|
7
|
+
* Register multiple webhook handlers at once
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerWebhookHandlers(handlerMap: Record<WebhookEventType, WebhookHandler>): void;
|
|
10
|
+
/**
|
|
11
|
+
* Construct and verify a webhook event from the raw request.
|
|
12
|
+
*
|
|
13
|
+
* `tolerance` (in seconds) controls how much clock skew between Stripe and
|
|
14
|
+
* this server is acceptable before signatures are rejected. Stripe's SDK
|
|
15
|
+
* default is 300s; tightening this is recommended in production but the
|
|
16
|
+
* previous code dropped the value entirely, so a configured tolerance was
|
|
17
|
+
* silently ignored.
|
|
18
|
+
*/
|
|
19
|
+
export declare function constructEvent(payload: string | Buffer, signature: string, secret: string, tolerance?: number): Stripe.Event;
|
|
20
|
+
/**
|
|
21
|
+
* Async variant of {@link constructEvent}. Some runtimes (notably Bun) resolve
|
|
22
|
+
* the Stripe SDK's crypto provider to WebCrypto's `SubtleCrypto`, which is
|
|
23
|
+
* async-only — the synchronous `constructEvent` then throws
|
|
24
|
+
* "SubtleCryptoProvider cannot be used in a synchronous context". Verifying via
|
|
25
|
+
* `constructEventAsync` works across Node and Bun, so webhook processing uses
|
|
26
|
+
* this path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function constructEventAsync(payload: string | Buffer, signature: string, secret: string, tolerance?: number): Promise<Stripe.Event>;
|
|
29
|
+
/**
|
|
30
|
+
* Handle an incoming webhook event
|
|
31
|
+
*/
|
|
32
|
+
export declare function handleWebhookEvent(event: Stripe.Event): Promise<{ handled: boolean, eventType: string, errors?: string[] }>;
|
|
33
|
+
/**
|
|
34
|
+
* Process a webhook request
|
|
35
|
+
*/
|
|
36
|
+
export declare function processWebhook(payload: string | Buffer, signature: string, config: WebhookConfig): Promise<{ success: boolean, eventType?: string, error?: string }>;
|
|
37
|
+
/**
|
|
38
|
+
* Register payment intent handlers
|
|
39
|
+
*/
|
|
40
|
+
export declare function onPaymentIntent(handlers: {
|
|
41
|
+
succeeded?: WebhookHandler
|
|
42
|
+
failed?: WebhookHandler
|
|
43
|
+
created?: WebhookHandler
|
|
44
|
+
canceled?: WebhookHandler
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Register subscription handlers
|
|
48
|
+
*/
|
|
49
|
+
export declare function onSubscription(handlers: {
|
|
50
|
+
created?: WebhookHandler
|
|
51
|
+
updated?: WebhookHandler
|
|
52
|
+
deleted?: WebhookHandler
|
|
53
|
+
trialWillEnd?: WebhookHandler
|
|
54
|
+
}): void;
|
|
55
|
+
/**
|
|
56
|
+
* Register invoice handlers
|
|
57
|
+
*/
|
|
58
|
+
export declare function onInvoice(handlers: {
|
|
59
|
+
paid?: WebhookHandler
|
|
60
|
+
paymentFailed?: WebhookHandler
|
|
61
|
+
created?: WebhookHandler
|
|
62
|
+
finalized?: WebhookHandler
|
|
63
|
+
}): void;
|
|
64
|
+
/**
|
|
65
|
+
* Register checkout session handlers
|
|
66
|
+
*/
|
|
67
|
+
export declare function onCheckout(handlers: {
|
|
68
|
+
completed?: WebhookHandler
|
|
69
|
+
expired?: WebhookHandler
|
|
70
|
+
}): void;
|
|
71
|
+
/**
|
|
72
|
+
* Register charge handlers
|
|
73
|
+
*/
|
|
74
|
+
export declare function onCharge(handlers: {
|
|
75
|
+
succeeded?: WebhookHandler
|
|
76
|
+
failed?: WebhookHandler
|
|
77
|
+
refunded?: WebhookHandler
|
|
78
|
+
disputed?: WebhookHandler
|
|
79
|
+
}): void;
|
|
80
|
+
/**
|
|
81
|
+
* Extract payment intent from event
|
|
82
|
+
*/
|
|
83
|
+
export declare function getPaymentIntent(event: Stripe.Event): Stripe.PaymentIntent | null;
|
|
84
|
+
/**
|
|
85
|
+
* Extract subscription from event
|
|
86
|
+
*/
|
|
87
|
+
export declare function getSubscription(event: Stripe.Event): Stripe.Subscription | null;
|
|
88
|
+
/**
|
|
89
|
+
* Extract invoice from event
|
|
90
|
+
*/
|
|
91
|
+
export declare function getInvoice(event: Stripe.Event): Stripe.Invoice | null;
|
|
92
|
+
/**
|
|
93
|
+
* Extract checkout session from event
|
|
94
|
+
*/
|
|
95
|
+
export declare function getCheckoutSession(event: Stripe.Event): Stripe.Checkout.Session | null;
|
|
96
|
+
/**
|
|
97
|
+
* Extract charge from event
|
|
98
|
+
*/
|
|
99
|
+
export declare function getCharge(event: Stripe.Event): Stripe.Charge | null;
|
|
100
|
+
/**
|
|
101
|
+
* Extract customer from event
|
|
102
|
+
*/
|
|
103
|
+
export declare function getCustomer(event: Stripe.Event): Stripe.Customer | null;
|
|
104
|
+
declare const handlers: Map<WebhookEventType, WebhookHandler[]>;
|
|
105
|
+
export declare const manageWebhook: {
|
|
106
|
+
onWebhookEvent: typeof onWebhookEvent;
|
|
107
|
+
registerWebhookHandlers: typeof registerWebhookHandlers;
|
|
108
|
+
constructEvent: typeof constructEvent;
|
|
109
|
+
constructEventAsync: typeof constructEventAsync;
|
|
110
|
+
handleWebhookEvent: typeof handleWebhookEvent;
|
|
111
|
+
processWebhook: typeof processWebhook;
|
|
112
|
+
onPaymentIntent: typeof onPaymentIntent;
|
|
113
|
+
onSubscription: typeof onSubscription;
|
|
114
|
+
onInvoice: typeof onInvoice;
|
|
115
|
+
onCheckout: typeof onCheckout;
|
|
116
|
+
onCharge: typeof onCharge;
|
|
117
|
+
getPaymentIntent: typeof getPaymentIntent;
|
|
118
|
+
getSubscription: typeof getSubscription;
|
|
119
|
+
getInvoice: typeof getInvoice;
|
|
120
|
+
getCheckoutSession: typeof getCheckoutSession;
|
|
121
|
+
getCharge: typeof getCharge;
|
|
122
|
+
getCustomer: typeof getCustomer
|
|
123
|
+
};
|
|
124
|
+
export declare interface WebhookConfig {
|
|
125
|
+
secret: string
|
|
126
|
+
tolerance?: number
|
|
127
|
+
}
|
|
128
|
+
export type WebhookEventType = | 'payment_intent.succeeded'
|
|
129
|
+
| 'payment_intent.payment_failed'
|
|
130
|
+
| 'payment_intent.created'
|
|
131
|
+
| 'payment_intent.canceled'
|
|
132
|
+
| 'customer.subscription.created'
|
|
133
|
+
| 'customer.subscription.updated'
|
|
134
|
+
| 'customer.subscription.deleted'
|
|
135
|
+
| 'customer.subscription.trial_will_end'
|
|
136
|
+
| 'customer.created'
|
|
137
|
+
| 'customer.updated'
|
|
138
|
+
| 'customer.deleted'
|
|
139
|
+
| 'invoice.paid'
|
|
140
|
+
| 'invoice.payment_failed'
|
|
141
|
+
| 'invoice.finalized'
|
|
142
|
+
| 'invoice.created'
|
|
143
|
+
| 'checkout.session.completed'
|
|
144
|
+
| 'checkout.session.expired'
|
|
145
|
+
| 'charge.succeeded'
|
|
146
|
+
| 'charge.failed'
|
|
147
|
+
| 'charge.refunded'
|
|
148
|
+
| 'charge.dispute.created'
|
|
149
|
+
| 'charge.dispute.closed'
|
|
150
|
+
| 'payment_method.attached'
|
|
151
|
+
| 'payment_method.detached'
|
|
152
|
+
| 'setup_intent.succeeded'
|
|
153
|
+
| 'setup_intent.setup_failed'
|
|
154
|
+
| string;
|
|
155
|
+
export type WebhookHandler = (_event: Stripe.Event) => Promise<void> | void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { stripe } from "..";
|
|
2
|
+
const handlers = new Map;
|
|
3
|
+
export function onWebhookEvent(eventType, handler) {
|
|
4
|
+
const existing = handlers.get(eventType) || [];
|
|
5
|
+
existing.push(handler);
|
|
6
|
+
handlers.set(eventType, existing);
|
|
7
|
+
}
|
|
8
|
+
export function registerWebhookHandlers(handlerMap) {
|
|
9
|
+
for (const [eventType, handler] of Object.entries(handlerMap))
|
|
10
|
+
onWebhookEvent(eventType, handler);
|
|
11
|
+
}
|
|
12
|
+
export function constructEvent(payload, signature, secret, tolerance) {
|
|
13
|
+
if (tolerance != null && Number.isFinite(tolerance) && tolerance > 0)
|
|
14
|
+
return stripe.webhooks.constructEvent(payload, signature, secret, tolerance);
|
|
15
|
+
return stripe.webhooks.constructEvent(payload, signature, secret);
|
|
16
|
+
}
|
|
17
|
+
export async function constructEventAsync(payload, signature, secret, tolerance) {
|
|
18
|
+
if (tolerance != null && Number.isFinite(tolerance) && tolerance > 0)
|
|
19
|
+
return await stripe.webhooks.constructEventAsync(payload, signature, secret, tolerance);
|
|
20
|
+
return await stripe.webhooks.constructEventAsync(payload, signature, secret);
|
|
21
|
+
}
|
|
22
|
+
export async function handleWebhookEvent(event) {
|
|
23
|
+
const eventHandlers = handlers.get(event.type) || [];
|
|
24
|
+
if (eventHandlers.length === 0)
|
|
25
|
+
return { handled: !1, eventType: event.type };
|
|
26
|
+
const errors = [];
|
|
27
|
+
for (const handler of eventHandlers)
|
|
28
|
+
try {
|
|
29
|
+
await handler(event);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
handled: !0,
|
|
35
|
+
eventType: event.type,
|
|
36
|
+
...errors.length > 0 ? { errors } : {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function processWebhook(payload, signature, config) {
|
|
40
|
+
try {
|
|
41
|
+
const event = await constructEventAsync(payload, signature, config.secret, config.tolerance), result = await handleWebhookEvent(event);
|
|
42
|
+
if (result.errors && result.errors.length > 0)
|
|
43
|
+
return {
|
|
44
|
+
success: !1,
|
|
45
|
+
eventType: result.eventType,
|
|
46
|
+
error: result.errors.join("; ")
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
success: !0,
|
|
50
|
+
eventType: result.eventType
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
success: !1,
|
|
55
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function onPaymentIntent(handlers) {
|
|
60
|
+
if (handlers.succeeded)
|
|
61
|
+
onWebhookEvent("payment_intent.succeeded", handlers.succeeded);
|
|
62
|
+
if (handlers.failed)
|
|
63
|
+
onWebhookEvent("payment_intent.payment_failed", handlers.failed);
|
|
64
|
+
if (handlers.created)
|
|
65
|
+
onWebhookEvent("payment_intent.created", handlers.created);
|
|
66
|
+
if (handlers.canceled)
|
|
67
|
+
onWebhookEvent("payment_intent.canceled", handlers.canceled);
|
|
68
|
+
}
|
|
69
|
+
export function onSubscription(handlers) {
|
|
70
|
+
if (handlers.created)
|
|
71
|
+
onWebhookEvent("customer.subscription.created", handlers.created);
|
|
72
|
+
if (handlers.updated)
|
|
73
|
+
onWebhookEvent("customer.subscription.updated", handlers.updated);
|
|
74
|
+
if (handlers.deleted)
|
|
75
|
+
onWebhookEvent("customer.subscription.deleted", handlers.deleted);
|
|
76
|
+
if (handlers.trialWillEnd)
|
|
77
|
+
onWebhookEvent("customer.subscription.trial_will_end", handlers.trialWillEnd);
|
|
78
|
+
}
|
|
79
|
+
export function onInvoice(handlers) {
|
|
80
|
+
if (handlers.paid)
|
|
81
|
+
onWebhookEvent("invoice.paid", handlers.paid);
|
|
82
|
+
if (handlers.paymentFailed)
|
|
83
|
+
onWebhookEvent("invoice.payment_failed", handlers.paymentFailed);
|
|
84
|
+
if (handlers.created)
|
|
85
|
+
onWebhookEvent("invoice.created", handlers.created);
|
|
86
|
+
if (handlers.finalized)
|
|
87
|
+
onWebhookEvent("invoice.finalized", handlers.finalized);
|
|
88
|
+
}
|
|
89
|
+
export function onCheckout(handlers) {
|
|
90
|
+
if (handlers.completed)
|
|
91
|
+
onWebhookEvent("checkout.session.completed", handlers.completed);
|
|
92
|
+
if (handlers.expired)
|
|
93
|
+
onWebhookEvent("checkout.session.expired", handlers.expired);
|
|
94
|
+
}
|
|
95
|
+
export function onCharge(handlers) {
|
|
96
|
+
if (handlers.succeeded)
|
|
97
|
+
onWebhookEvent("charge.succeeded", handlers.succeeded);
|
|
98
|
+
if (handlers.failed)
|
|
99
|
+
onWebhookEvent("charge.failed", handlers.failed);
|
|
100
|
+
if (handlers.refunded)
|
|
101
|
+
onWebhookEvent("charge.refunded", handlers.refunded);
|
|
102
|
+
if (handlers.disputed)
|
|
103
|
+
onWebhookEvent("charge.dispute.created", handlers.disputed);
|
|
104
|
+
}
|
|
105
|
+
export function getPaymentIntent(event) {
|
|
106
|
+
if (event.type.startsWith("payment_intent."))
|
|
107
|
+
return event.data.object;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
export function getSubscription(event) {
|
|
111
|
+
if (event.type.startsWith("customer.subscription."))
|
|
112
|
+
return event.data.object;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
export function getInvoice(event) {
|
|
116
|
+
if (event.type.startsWith("invoice."))
|
|
117
|
+
return event.data.object;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
export function getCheckoutSession(event) {
|
|
121
|
+
if (event.type.startsWith("checkout.session."))
|
|
122
|
+
return event.data.object;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
export function getCharge(event) {
|
|
126
|
+
if (event.type.startsWith("charge."))
|
|
127
|
+
return event.data.object;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
export function getCustomer(event) {
|
|
131
|
+
if (event.type.startsWith("customer.") && !event.type.includes("subscription"))
|
|
132
|
+
return event.data.object;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
export const manageWebhook = {
|
|
136
|
+
onWebhookEvent,
|
|
137
|
+
registerWebhookHandlers,
|
|
138
|
+
constructEvent,
|
|
139
|
+
constructEventAsync,
|
|
140
|
+
handleWebhookEvent,
|
|
141
|
+
processWebhook,
|
|
142
|
+
onPaymentIntent,
|
|
143
|
+
onSubscription,
|
|
144
|
+
onInvoice,
|
|
145
|
+
onCheckout,
|
|
146
|
+
onCharge,
|
|
147
|
+
getPaymentIntent,
|
|
148
|
+
getSubscription,
|
|
149
|
+
getInvoice,
|
|
150
|
+
getCheckoutSession,
|
|
151
|
+
getCharge,
|
|
152
|
+
getCustomer
|
|
153
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Stripe from 'stripe';
|
|
2
|
+
/**
|
|
3
|
+
* Lazy-initialized Stripe instance.
|
|
4
|
+
* Only throws when you actually try to use Stripe, not at module load time.
|
|
5
|
+
* This allows the payments package to be imported without a configured key
|
|
6
|
+
* (e.g., in tests, CLI, or environments that don't use Stripe).
|
|
7
|
+
*/
|
|
8
|
+
export declare const stripe: Stripe;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { services } from "@stacksjs/config";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
function StripeCtor() {
|
|
5
|
+
try {
|
|
6
|
+
return require("stripe");
|
|
7
|
+
} catch {
|
|
8
|
+
throw Error("Stripe payments are being used but the `stripe` package is not installed. " + "It is an opt-in dependency \u2014 run `bun add stripe` to enable server-side Stripe payments.");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
let _stripe = null;
|
|
12
|
+
export const stripe = new Proxy({}, {
|
|
13
|
+
get(_target, prop) {
|
|
14
|
+
if (!_stripe) {
|
|
15
|
+
const apiKey = services?.stripe?.secretKey;
|
|
16
|
+
if (!apiKey)
|
|
17
|
+
throw Error("Stripe secret key is not configured. Set STRIPE_SECRET_KEY in your .env file.");
|
|
18
|
+
const apiVersion = "2026-03-25.dahlia", configuredVersion = services?.stripe?.apiVersion;
|
|
19
|
+
if (configuredVersion && configuredVersion !== apiVersion)
|
|
20
|
+
throw Error(`Stripe API version ${configuredVersion} does not match the installed SDK version ${apiVersion}`);
|
|
21
|
+
_stripe = new (StripeCtor())(apiKey, { apiVersion });
|
|
22
|
+
}
|
|
23
|
+
return _stripe[prop];
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a deterministic Stripe idempotency key for an operation.
|
|
3
|
+
* The `scope` should be a stable string identifying the call site
|
|
4
|
+
* (e.g. `'customer.create'`, `'subscription.create'`); `parts` are
|
|
5
|
+
* any extra identifying values (user id, lookup key, etc.) that
|
|
6
|
+
* uniquely scope the operation within that user's lifetime.
|
|
7
|
+
*
|
|
8
|
+
* @param scope - stable operation name; never user input
|
|
9
|
+
* @param parts - identifying values appended after scope (coerced
|
|
10
|
+
* to string; nullish values are skipped)
|
|
11
|
+
*/
|
|
12
|
+
export declare function stacksIdempotencyKey(scope: string, ...parts: Array<string | number | null | undefined>): string;
|
|
13
|
+
/**
|
|
14
|
+
* Build a one-shot idempotency key for operations that are NOT
|
|
15
|
+
* naturally retryable (a unique-per-attempt key that won't collide
|
|
16
|
+
* with anything). Used when the caller WANTS a fresh Stripe object
|
|
17
|
+
* each time but still wants the safety of a single network retry
|
|
18
|
+
* within a single attempt. The key includes a random suffix so
|
|
19
|
+
* sequential attempts produce different keys.
|
|
20
|
+
*
|
|
21
|
+
* Used for payment intents, checkout sessions — operations where
|
|
22
|
+
* "create another one" is the right semantic on a retry by the
|
|
23
|
+
* caller, but where a single network blip during a single attempt
|
|
24
|
+
* still benefits from in-flight idempotency.
|
|
25
|
+
*/
|
|
26
|
+
export declare function freshIdempotencyKey(scope: string, ...parts: Array<string | number | null | undefined>): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const KEY_PREFIX = "stacks", KEY_VERSION = "v1", MAX_KEY_LENGTH = 255;
|
|
3
|
+
export function stacksIdempotencyKey(scope, ...parts) {
|
|
4
|
+
const tail = parts.filter((p) => p !== null && p !== void 0 && p !== "").map((p) => String(p)).join(":"), raw = tail ? `${KEY_PREFIX}:${scope}:${tail}:${KEY_VERSION}` : `${KEY_PREFIX}:${scope}:${KEY_VERSION}`;
|
|
5
|
+
if (raw.length <= MAX_KEY_LENGTH)
|
|
6
|
+
return raw;
|
|
7
|
+
const hashed = createHash("sha256").update(tail).digest("hex").slice(0, 32);
|
|
8
|
+
return `${KEY_PREFIX}:${scope}:${hashed}:${KEY_VERSION}`;
|
|
9
|
+
}
|
|
10
|
+
export function freshIdempotencyKey(scope, ...parts) {
|
|
11
|
+
const random = createHash("sha256").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16);
|
|
12
|
+
return stacksIdempotencyKey(scope, ...parts, random);
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type * as Stripe from 'stripe';
|
|
2
|
+
/**
|
|
3
|
+
* Payments Package
|
|
4
|
+
*
|
|
5
|
+
* Provides payment processing capabilities with Stripe integration.
|
|
6
|
+
*/
|
|
7
|
+
// Main Payment facade
|
|
8
|
+
export * from './payment';
|
|
9
|
+
export { default as Payment } from './payment';
|
|
10
|
+
// Billable modules
|
|
11
|
+
export * from './billable/index';
|
|
12
|
+
// Stripe driver and SDK
|
|
13
|
+
export * from './drivers/stripe';
|
|
14
|
+
// Idempotency-key helpers — passed to every Stripe create/update so
|
|
15
|
+
// retries don't produce duplicate objects (stacksjs/stacks#1876 X-1).
|
|
16
|
+
export { freshIdempotencyKey, stacksIdempotencyKey } from './idempotency';
|