import { SubscriptionPeriod, Tier } from '@prisma/client';
import { APIGatewayProxyEventV2 } from 'aws-lambda';
import Stripe from 'stripe';
import { HTTP_CODE } from '@libs/constants';
import db from '@libs/db';
import { stripe } from '@libs/stripe';
export async function _stripeWebhooksHandler(_event: APIGatewayProxyEventV2) {
const stripeSignature = _event.headers['stripe-signature'];
if (!stripeSignature) {
return {
status: HTTP_CODE.BAD_REQUEST,
message: 'No Stripe signature provided',
};
}
try {
const stripeEvent = await stripe.webhooks.constructEventAsync(
// we need to pass the raw body of the event. Converting it into JSON or any other form will break the code.
// Luckily, we get raw body in `_event` by default, so, we do not need to do anything extra.
_event.body!,
stripeSignature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
switch (stripeEvent.type) {
// For my particular case, I only needed to take into account the following event types.
// For you, it might be different. Please refer to https://docs.stripe.com/api/events/types
case 'customer.subscription.created':
return handleSubscriptionEvent(stripeEvent, 'created');
case 'customer.subscription.updated':
return handleSubscriptionEvent(stripeEvent, 'updated');
case 'customer.subscription.deleted':
return handleSubscriptionEvent(stripeEvent, 'deleted');
case 'invoice.payment_succeeded':
return handleInvoiceEvent(stripeEvent, 'succeeded');
case 'invoice.payment_failed':
return handleInvoiceEvent(stripeEvent, 'failed');
case 'checkout.session.completed':
return handleCheckoutSessionCompleted(stripeEvent);
default:
return {
status: HTTP_CODE.BAD_REQUEST,
message: 'Unhandled event type',
};
}
} catch (err) {
console.error('Error constructing Stripe event:', err);
return {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Webhook Error: Invalid Signature',
};
}
}
async function handleSubscriptionEvent(
event: Stripe.Event,
type: 'created' | 'updated' | 'deleted',
) {
const subscription = event.data.object as Stripe.Subscription;
const customerEmail = await getCustomerEmail(subscription.customer as string);
if (!customerEmail) {
return {
status: HTTP_CODE.BAD_REQUEST,
message: 'Missing userId, customer email, or plan in subscription',
};
}
if (subscription.cancel_at_period_end) {
await db.subscriptions
.update({
where: {
subscriptionId: subscription.id,
},
data: {
status: 'scheduled_for_cancellation',
},
})
.catch((err) => {
console.error('Error updating subscription status: ', err);
throw {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Error updating subscription status',
};
});
return {
status: HTTP_CODE.OK,
message:
'Subscription scheduled to be cancelled at the period end successfully',
};
} else if (type === 'deleted') {
const result = await db.subscriptions
.update({
where: {
subscriptionId: subscription.id,
},
data: {
status: 'cancelled',
},
})
.catch((err) => {
console.error('Error updating subscription status: ', err);
throw {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Error updating subscription status',
};
});
if (result) {
await db.user
.update({
where: {
email: customerEmail,
},
data: {
Tier: Tier.Free,
},
})
.catch((err) => {
console.error('Error updating user subscription status: ', err);
throw {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Error updating user subscription status',
};
});
}
return {
status: HTTP_CODE.OK,
message: 'Subscription cancelled successfully',
};
} else {
const createdAt = new Date(subscription.created * 1000).toISOString();
const subscriptionData: any = {
subscriptionId: subscription.id,
stripeUserId: subscription.customer,
status: subscription.status,
startDate: createdAt,
email: customerEmail,
planId: subscription.items.data[0]?.price.id,
defaultPaymentMethodId: subscription.default_payment_method as string,
createdAt,
};
await db.subscriptions
.upsert({
where: {
subscriptionId: subscription.id,
},
create: subscriptionData,
update: subscriptionData,
})
.catch((err) => {
console.error(`Error during subscription ${type}: ${err}`);
throw {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: `Error during subscription ${type}`,
};
});
return {
status: HTTP_CODE.OK,
message: 'Subscription updated successfully',
};
}
}
async function getCustomerEmail(customerId: string) {
try {
const customer = await stripe.customers.retrieve(customerId);
return (customer as Stripe.Customer).email;
} catch (error) {
console.error('Error fetching customer:', error);
return null;
}
}
async function handleInvoiceEvent(
event: Stripe.Event,
status: 'succeeded' | 'failed',
) {
const invoice = event.data.object as Stripe.Invoice;
const customerEmail = await getCustomerEmail(invoice.customer as string);
if (!customerEmail) {
return {
status: HTTP_CODE.BAD_REQUEST,
message: 'Customer email could not be fetched',
};
}
try {
const invoiceData: any = {
invoiceId: invoice.id,
subscriptionId: invoice.subscription as string,
amountPaid:
status === 'succeeded' ? (invoice.amount_paid / 100).toString() : '0',
amountDue:
status === 'failed' ? (invoice.amount_due / 100).toString() : '0',
currency: invoice.currency,
invoiceUrl: invoice.hosted_invoice_url,
status,
};
await db.invoices.upsert({
where: {
invoiceId: invoice.id,
},
create: invoiceData,
update: invoiceData,
});
return {
status: HTTP_CODE.OK,
message: `Invoice payment ${status}: ${invoiceData.invoiceUrl}`,
};
} catch (err) {
console.error('Error handling invoice event:', err);
return {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Error handling invoice event',
};
}
}
async function handleCheckoutSessionCompleted(event: Stripe.Event) {
const session = event.data.object as Stripe.Checkout.Session;
const metadata = session.metadata;
const userId = metadata?.userId;
const isMonthly =
metadata?.isMonthly === 'false'
? SubscriptionPeriod.yearly
: SubscriptionPeriod.monthly;
const plan = metadata?.plan as Tier;
const endDate = new Date();
if (isMonthly === SubscriptionPeriod.monthly) {
endDate.setMonth(endDate.getMonth() + 1);
} else if (isMonthly === SubscriptionPeriod.yearly) {
endDate.setFullYear(endDate.getFullYear() + 1);
} else {
return {
status: HTTP_CODE.BAD_REQUEST,
message: 'Invalid subscription period',
};
}
try {
const subscriptionId = session.subscription as string;
await stripe.subscriptions.update(subscriptionId as string, { metadata });
await db.subscriptions.update({
where: {
subscriptionId,
},
data: {
planPeriod: isMonthly,
endDate: endDate.toISOString(),
plan,
},
});
await db.user.update({
where: {
id: userId,
},
data: {
Tier: plan,
},
});
return {
status: HTTP_CODE.OK,
message: 'Subscription metadata updated successfully',
};
} catch (err) {
console.error(`Error handling checkout session: ${err}`);
return {
status: HTTP_CODE.INTERNAL_SERVER_ERROR,
message: 'Error handling checkout session',
};
}
}