Untitled
unknown
plain_text
2 years ago
13 kB
10
Indexable
"use node";
import { api, internal } from "../_generated/api";
import { action } from "../_generated/server";
import Stripe from "stripe";
import {
logSlackPayment,
sendSlackHelp,
withErrorLog,
logSlackError,
} from "./utils/slackLogger";
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY as string,
{ timeout: 20 * 1000, maxNetworkRetries: 2 } as any
);
export const createCheckoutSession = action(
async ({ runQuery, runMutation }, { quantity }: { quantity: number }) => {
return await withErrorLog(async () => {
const user = await runQuery(api.users.getUser);
const userId = user?._id;
if (!userId) {
throw new Error("No user found");
}
let id = null;
if (!user.stripeCustomerId || user.stripeCustomerId === "") {
const customer: Stripe.Response<Stripe.Customer> =
await stripe.customers.create({
email: user.email,
name: user.name,
metadata: {
userId: user._id.toString(),
},
});
if (!customer) {
throw new Error("Failed to create customer");
}
await runMutation(api.users.updateUserStripeCustomerId, {
stripeCustomerId: customer.id,
});
id = customer.id;
}
if (!id) {
id = user.stripeCustomerId;
}
const prices = await stripe.prices.list({
active: true,
limit: 3,
});
const session: Stripe.Response<Stripe.Checkout.Session> =
await stripe.checkout.sessions.create({
line_items: [
{
price: prices.data[0].id,
quantity,
},
],
mode: "subscription",
success_url: `${process.env.DOMAIN}/plans/success`,
cancel_url: `${process.env.DOMAIN}/plans`,
customer: id,
allow_promotion_codes: true,
});
return session.id;
}, "stripe:ts: createCheckoutSession");
}
);
export const createCheckoutSessionOnImport = action(
async ({ runQuery, runMutation }, { quantity }: { quantity: number }) => {
return await withErrorLog(async () => {
const user = await runQuery(api.users.getUser);
const userId = user?._id;
if (!userId) {
throw new Error("No user found");
}
let id = null;
if (!user.stripeCustomerId || user.stripeCustomerId === "") {
const customer: Stripe.Response<Stripe.Customer> =
await stripe.customers.create({
email: user.email,
name: user.name,
metadata: {
userId: user._id.toString(),
},
});
if (!customer) {
throw new Error("Failed to create customer");
}
await runMutation(api.users.updateUserStripeCustomerId, {
stripeCustomerId: customer.id,
});
id = customer.id;
}
if (!id) {
id = user.stripeCustomerId;
}
const prices = await stripe.prices.list({
active: true,
limit: 3,
});
const session: Stripe.Response<Stripe.Checkout.Session> =
await stripe.checkout.sessions.create({
line_items: [
{
price: prices.data[0].id,
quantity,
},
],
mode: "subscription",
success_url: `${process.env.DOMAIN}/listings/add`,
cancel_url: `${process.env.DOMAIN}/listings/add`,
customer: id,
allow_promotion_codes: true,
});
return session.id;
}, "stripe:ts: createCheckoutSessionOnImport");
}
);
export const createPortalSession = action(async ({ runQuery }) => {
return await withErrorLog(async () => {
const subscription = await runQuery(api.subscriptions.get);
if (!subscription) {
throw new Error("No subscription found");
}
const session: Stripe.Response<Stripe.BillingPortal.Session> =
await stripe.billingPortal.sessions.create({
customer: subscription.stripeCustomerId,
return_url: `${process.env.DOMAIN}/plans`,
});
return session.url;
}, "stripe:ts: createPortalSession");
});
export const updateSubscriptionQuantity = action(
async ({ runQuery }, { quantity }: { quantity: number }) => {
return await withErrorLog(async () => {
const subscription = await runQuery(api.subscriptions.get);
if (!subscription) {
throw new Error("No subscription found");
}
const customerSubscriptionIds = await stripe.subscriptionItems.list(
{
subscription: subscription.subscriptionId,
}
);
const updatedSubscription: Stripe.Response<Stripe.Subscription> =
await stripe.subscriptions.update(subscription.subscriptionId, {
items: [
{
id: customerSubscriptionIds.data[0].id,
price: subscription.priceId,
quantity,
},
],
});
return updatedSubscription;
}, "stripe:ts: updateSubscriptionQuantity");
}
);
export const receiveWebhook = action(
async (
{ runQuery, runAction, runMutation },
{ body, sig }: { body: string; sig: string }
): Promise<{ status: number; message: string }> => {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Unknown error";
// On error, log and return the error message.
if (err! instanceof Error) console.log(err);
console.log(`❌ Error message: ${errorMessage}`);
return { status: 400, message: `Webhook Error: ${errorMessage}` };
}
// Successfully constructed event.
console.log("✅ Success:", event.id);
let message = "";
// Cast event data to Stripe object.
try {
switch (event.type) {
case "payment_intent.succeeded": {
const paymentIntent = event.data
.object as Stripe.PaymentIntent;
message = `💰 PaymentIntent status: ${paymentIntent.status}`;
break;
}
case "payment_intent.payment_failed": {
const paymentIntent = event.data
.object as Stripe.PaymentIntent;
message = `❌ Payment failed: ${paymentIntent.last_payment_error?.message}`;
break;
}
case "customer.subscription.created": {
const subscription = event.data
.object as Stripe.Subscription;
await runMutation(api.subscriptions.create, {
subscriptionObj: subscription,
});
message = `💰 Subscription created: ${subscription.id}`;
const subcriptionItem = await runQuery(
internal.subscriptions.getByCustomerId,
{
customerId: subscription.customer as string,
}
);
const userId = subcriptionItem?.userId;
if (userId && process.env.ENVIROMENT === "production") {
await runAction(
internal.actions.utils.airtable
.updateAirtablePaidAndQuantityByUser,
{
userId,
paid: true,
quantity: (subscription as any).quantity,
}
);
}
if (process.env.ENVIRONMENT == "production")
await logSlackPayment(message);
break;
}
case "customer.subscription.updated": {
const subscription = event.data
.object as Stripe.Subscription;
await runMutation(api.subscriptions.update, {
subscriptionObj: subscription,
});
message = `🔔 Subscription updated: ${subscription.id}`;
const subscriptionItem = await runQuery(
internal.subscriptions.getByCustomerId,
{
customerId: subscription.customer as string,
}
);
const userId = subscriptionItem?.userId;
if (userId && process.env.ENVIROMENT === "production") {
await runAction(
internal.actions.utils.airtable
.updateAirtablePaidAndQuantityByUser,
{
userId,
paid: !subscription.cancel_at_period_end,
quantity: (subscription as any).quantity,
}
);
}
break;
}
case "customer.subscription.deleted": {
const subscription = event.data
.object as Stripe.Subscription;
await runMutation(api.subscriptions.deleteSub, {
customerId: subscription.customer as string,
});
message = `🔔 Subscription deleted: ${subscription.id}`;
const subscriptionItem = await runQuery(
internal.subscriptions.getByCustomerId,
{
customerId: subscription.customer as string,
}
);
const userId = subscriptionItem?.userId;
if (userId && process.env.ENVIROMENT === "production") {
await runAction(
internal.actions.utils.airtable
.updateAirtablePaidAndQuantityByUser,
{
userId,
paid: false,
quantity: (subscription as any).quantity,
}
);
}
break;
}
case "invoice.created": {
const invoice = event.data.object as Stripe.Invoice;
message = `🔔 Invoice created: ${invoice.id}`;
break;
}
case "invoice.paid": {
const invoice = event.data.object as Stripe.Invoice;
message = `💰 Invoice paid: ${invoice.id}`;
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
message = `❌ Invoice payment failed: ${invoice.id}`;
break;
}
default: {
message = `🤷♀️ Unhandled event type: ${event.type}`;
console.warn(message);
break;
}
}
console.log(message);
} catch (e: any) {
await logSlackError(e.message, "stripe.ts: receiveWebhook");
return { status: 400, message: `Webhook Error: ${e}` };
}
// Return a response to acknowledge receipt of the event.
return { status: 200, message: "success" };
}
);
Editor is loading...
Leave a Comment