Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(billing): Implement last month usage charging on subscription cancellation #296

Merged
merged 10 commits into from
Feb 7, 2025
21 changes: 21 additions & 0 deletions app/webhooks/stripe/handle-invoice-creation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { stripe } from "@/services/external/stripe";
import type Stripe from "stripe";

export async function handleInvoiceCreation(invoice: Stripe.Invoice) {
if (!invoice.subscription || typeof invoice.subscription !== "string") {
throw new Error(
"Invoice is missing a subscription ID. Please check the invoice data.",
);
}

const subscription = await stripe.subscriptions.retrieve(
invoice.subscription,
);

if (subscription.status !== "canceled") {
return;
}

// When a subscription is canceled, we should charge for usage-based billing from the previous billing cycle. The final invoice, which includes these charges, will be automatically created but will not be processed for payment. Therefore, we need to handle this case manually.
await stripe.invoices.pay(invoice.id);
}
20 changes: 3 additions & 17 deletions app/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { stripe } from "@/services/external/stripe";
import { upsertSubscription } from "@/services/external/stripe/actions/upsert-subscription";
import { reportUserSeatUsage } from "@/services/usage-based-billing";
import type Stripe from "stripe";
import { handleInvoiceCreation } from "./handle-invoice-creation";
import { handleSubscriptionCancellation } from "./handle-subscription-cancellation";

const relevantEvents = new Set([
Expand Down Expand Up @@ -91,28 +92,13 @@ export async function POST(req: Request) {
);
}
await handleSubscriptionCancellation(event.data.object);
await upsertSubscription(event.data.object.id);
Copy link
Contributor Author

@gentamura gentamura Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 When a subscription expires and the subscription is cancelled, customer.subscription.updated does not fire, so the above method is executed on customer.subscription.deleted and the database is updated appropriately.

break;

case "invoice.created":
console.log(`🔔 Invoice created: ${event.data.object.id}`);

// TODO: Skip for now - will be handled when implementing subscription cancellation invoice processing
if (
event.data.object.subscription &&
typeof event.data.object.subscription === "string"
) {
const subscriptionId = event.data.object.subscription;
const subscription =
await stripe.subscriptions.retrieve(subscriptionId);

if (subscription.status === "canceled") {
console.log(
"Skipping processing for canceled subscription invoice: ",
subscriptionId,
);
break;
}
}
await handleInvoiceCreation(event.data.object);
break;

default:
Expand Down