first
This commit is contained in:
commit
8538ab969a
434 changed files with 44051 additions and 0 deletions
254
packages/payments/provider/stripe/index.ts
Normal file
254
packages/payments/provider/stripe/index.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import {
|
||||
createPurchase,
|
||||
deletePurchaseBySubscriptionId,
|
||||
getPurchaseBySubscriptionId,
|
||||
updatePurchase,
|
||||
} from "@repo/database";
|
||||
import { logger } from "@repo/logs";
|
||||
import Stripe from "stripe";
|
||||
import { setCustomerIdToEntity } from "../../src/lib/customer";
|
||||
import type {
|
||||
CancelSubscription,
|
||||
CreateCheckoutLink,
|
||||
CreateCustomerPortalLink,
|
||||
SetSubscriptionSeats,
|
||||
WebhookHandler,
|
||||
} from "../../types";
|
||||
|
||||
let stripeClient: Stripe | null = null;
|
||||
|
||||
export function getStripeClient() {
|
||||
if (stripeClient) {
|
||||
return stripeClient;
|
||||
}
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY as string;
|
||||
|
||||
if (!stripeSecretKey) {
|
||||
throw new Error("Missing env variable STRIPE_SECRET_KEY");
|
||||
}
|
||||
|
||||
stripeClient = new Stripe(stripeSecretKey);
|
||||
|
||||
return stripeClient;
|
||||
}
|
||||
|
||||
export const createCheckoutLink: CreateCheckoutLink = async (options) => {
|
||||
const stripeClient = getStripeClient();
|
||||
const {
|
||||
type,
|
||||
productId,
|
||||
redirectUrl,
|
||||
customerId,
|
||||
organizationId,
|
||||
userId,
|
||||
trialPeriodDays,
|
||||
seats,
|
||||
email,
|
||||
} = options;
|
||||
|
||||
const metadata = {
|
||||
organization_id: organizationId || null,
|
||||
user_id: userId || null,
|
||||
};
|
||||
|
||||
const response = await stripeClient.checkout.sessions.create({
|
||||
mode: type === "subscription" ? "subscription" : "payment",
|
||||
success_url: redirectUrl ?? "",
|
||||
line_items: [
|
||||
{
|
||||
quantity: seats ?? 1,
|
||||
price: productId,
|
||||
},
|
||||
],
|
||||
customer: customerId,
|
||||
customer_email: email,
|
||||
...(type === "one-time"
|
||||
? {
|
||||
payment_intent_data: {
|
||||
metadata,
|
||||
},
|
||||
customer_creation: "always",
|
||||
}
|
||||
: {
|
||||
subscription_data: {
|
||||
metadata,
|
||||
trial_period_days: trialPeriodDays,
|
||||
},
|
||||
}),
|
||||
metadata,
|
||||
});
|
||||
|
||||
return response.url;
|
||||
};
|
||||
|
||||
export const createCustomerPortalLink: CreateCustomerPortalLink = async ({
|
||||
customerId,
|
||||
redirectUrl,
|
||||
}) => {
|
||||
const stripeClient = getStripeClient();
|
||||
|
||||
const response = await stripeClient.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: redirectUrl ?? "",
|
||||
});
|
||||
|
||||
return response.url;
|
||||
};
|
||||
|
||||
export const setSubscriptionSeats: SetSubscriptionSeats = async ({
|
||||
id,
|
||||
seats,
|
||||
}) => {
|
||||
const stripeClient = getStripeClient();
|
||||
|
||||
const subscription = await stripeClient.subscriptions.retrieve(id);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("Subscription not found.");
|
||||
}
|
||||
|
||||
await stripeClient.subscriptions.update(id, {
|
||||
items: [
|
||||
{
|
||||
id: subscription.items.data[0].id,
|
||||
quantity: seats,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const cancelSubscription: CancelSubscription = async (id) => {
|
||||
const stripeClient = getStripeClient();
|
||||
|
||||
await stripeClient.subscriptions.cancel(id);
|
||||
};
|
||||
|
||||
export const webhookHandler: WebhookHandler = async (req) => {
|
||||
const stripeClient = getStripeClient();
|
||||
|
||||
if (!req.body) {
|
||||
return new Response("Invalid request.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
let event: Stripe.Event | undefined;
|
||||
|
||||
try {
|
||||
event = await stripeClient.webhooks.constructEventAsync(
|
||||
await req.text(),
|
||||
req.headers.get("stripe-signature") as string,
|
||||
process.env.STRIPE_WEBHOOK_SECRET as string,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
return new Response("Invalid request.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const { mode, metadata, customer, id } = event.data.object;
|
||||
|
||||
if (mode === "subscription") {
|
||||
break;
|
||||
}
|
||||
|
||||
const checkoutSession =
|
||||
await stripeClient.checkout.sessions.retrieve(id, {
|
||||
expand: ["line_items"],
|
||||
});
|
||||
|
||||
const productId = checkoutSession.line_items?.data[0].price?.id;
|
||||
|
||||
if (!productId) {
|
||||
return new Response("Missing product ID.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
await createPurchase({
|
||||
organizationId: metadata?.organization_id || null,
|
||||
userId: metadata?.user_id || null,
|
||||
customerId: customer as string,
|
||||
type: "ONE_TIME",
|
||||
productId,
|
||||
});
|
||||
|
||||
await setCustomerIdToEntity(customer as string, {
|
||||
organizationId: metadata?.organization_id,
|
||||
userId: metadata?.user_id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.created": {
|
||||
const { metadata, customer, items, id } = event.data.object;
|
||||
|
||||
const productId = items?.data[0].price?.id;
|
||||
|
||||
if (!productId) {
|
||||
return new Response("Missing product ID.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
await createPurchase({
|
||||
subscriptionId: id,
|
||||
organizationId: metadata?.organization_id || null,
|
||||
userId: metadata?.user_id || null,
|
||||
customerId: customer as string,
|
||||
type: "SUBSCRIPTION",
|
||||
productId,
|
||||
status: event.data.object.status,
|
||||
});
|
||||
|
||||
await setCustomerIdToEntity(customer as string, {
|
||||
organizationId: metadata?.organization_id,
|
||||
userId: metadata?.user_id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.updated": {
|
||||
const subscriptionId = event.data.object.id;
|
||||
|
||||
const existingPurchase =
|
||||
await getPurchaseBySubscriptionId(subscriptionId);
|
||||
|
||||
if (existingPurchase) {
|
||||
await updatePurchase({
|
||||
id: existingPurchase.id,
|
||||
status: event.data.object.status,
|
||||
productId: event.data.object.items?.data[0].price?.id,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.deleted": {
|
||||
await deletePurchaseBySubscriptionId(event.data.object.id);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return new Response("Unhandled event type.", {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
`Webhook error: ${error instanceof Error ? error.message : ""}`,
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue