Untitled

 avatar
unknown
plain_text
2 years ago
13 kB
7
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