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

Conversation

gentamura
Copy link
Contributor

@gentamura gentamura commented Jan 7, 2025

Summary

Implement proper final billing handling when a subscription is cancelled, ensuring accurate billing for the last period of usage and maintaining data consistency.

Related issue

NONE

Changes

  • Added handleInvoiceCreation module to manage invoice processing
  • Added pay function for proper invoice state transitions
  • Added subscription data synchronisation after cancellation

Testing

  • Subscription cancellation flow tested
  • Verified correct invoice state transitions (draft → paid)
  • Confirmed database synchronisation after subscription cancellation

Other information

  • Billing state transitions follow Stripe's documentation: https://support.stripe.com/questions/invoice-states

  • Test clock simulation can be enabled for development testing

  • Adding the agent time charge manually is both time-consuming and expensive, so it can be done in the stripe cli as follows. As this is only a registration to stripe, the db is not updated, so the ui usage of the application remains unchanged.

stripe billing meter_events create \
  --event-name=agent_time_charge \
  -d ‘payload[value]’=256 \
  -d ‘payload[stripe_customer_id]’={{customer_id}}

Add STRIPE_TEST_CLOCK_ENABLED flag to control timestamp generation behavior:
- In test mode: Use current time for simulation
- In production: Use period end time minus buffer for accurate billing

This change improves testing capabilities while maintaining proper
production behavior for usage-based billing calculations.
…bscriptions

Remove redundant comments and consolidate invoice handling logic:
- Remove TODO comment and old conditional structure
- Replace with cleaner invoice validation check
- Prepare for implementing proper subscription cancellation processing

The changes streamline the webhook handler code while maintaining
functionality for canceled subscription scenarios.
Copy link

vercel bot commented Jan 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
giselle ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 6, 2025 5:19am

… handling

Split invoice processing into smaller, focused functions:
- Extract finalizeAndPayInvoice into separate function
- Add early return for non-canceled subscriptions
- Improve code organization with clear responsibility separation

This refactor enhances error handling and makes the code more
maintainable by following single responsibility principle.
@gentamura gentamura force-pushed the feat/charge-last-month-usage-on-subscription-deletion branch from d79ae15 to edfa1aa Compare January 7, 2025 04:17
@@ -82,33 +83,20 @@ 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.


async function finalizeAndPayInvoice(invoiceId: string) {
try {
await stripe.invoices.finalizeInvoice(invoiceId);
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.

📝 Update from Draft to Open with this method.

ref: https://support.stripe.com/questions/invoice-states?locale=en-US

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use auto_advance parameter here?
If we can, we would skip to call invoices.pay, invoices.send..., or so manually.
Stripe would care them.

https://docs.stripe.com/api/invoices/finalize?api-version=2024-11-20.acacia#finalize_invoice-auto_advance

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@satococoa

I will keep you posted on the progress along the way.

I ran it with await stripe.invoices.finalizeInvoice(invoice.id, { auto_advance: true }) and the flow was as follows.

  • Simulation proceeds until the end of the period and the invoice is updated from draft to open for the metered invoice.
  • automatic collection of the invoice is executed one hour after the invoice is issued, so the simulation proceeds to one hour later
  • Assumed to be paid at this time, but remained open previously and payment is not completed

Continue to check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@satococoa
It seems that running auto_advance: true in finalizeInvoice after cancelling a subscription is not working correctly (it works like auto_advance: false), so I also checked with Stripe support and found the following method to run. The following method is called

await stripe.invoices.pay(invoice.id);

If the subscription is cancelled and there are usage fees remaining, a draft invoice for the usage fees will be created at the end of the subscription period.
Based on this invoice, the payment method is executed to complete the billing process and execute the actual payment.

Add upsertSubscription call after handleSubscriptionCancellation to ensure subscription data is properly synchronized in the database when a subscription is canceled.

This ensures our system maintains accurate subscription state after cancellation processing.
@gentamura gentamura force-pushed the feat/charge-last-month-usage-on-subscription-deletion branch from a10f8f9 to 6ddee1e Compare January 7, 2025 05:44
@gentamura gentamura changed the title Feat/charge last month usage on subscription deletion feat(billing): Implement last month usage charging on subscription cancellation Jan 7, 2025
@gentamura gentamura self-assigned this Jan 7, 2025
@gentamura gentamura requested a review from satococoa January 7, 2025 06:41
@gentamura gentamura marked this pull request as ready for review January 7, 2025 06:41
@gentamura
Copy link
Contributor Author

@satococoa review, please 🙏

Copy link
Contributor

@satococoa satococoa left a comment

Choose a reason for hiding this comment

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

Thank you!
Could you check my comments, please?


async function finalizeAndPayInvoice(invoiceId: string) {
try {
await stripe.invoices.finalizeInvoice(invoiceId);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use auto_advance parameter here?
If we can, we would skip to call invoices.pay, invoices.send..., or so manually.
Stripe would care them.

https://docs.stripe.com/api/invoices/finalize?api-version=2024-11-20.acacia#finalize_invoice-auto_advance

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, but you do not have to care about this in this pull request.
Because I changed user sear reporting logic in #294.

- Remove unnecessary finalization step before payment
- Directly process invoice payment
- Remove error handling wrapper for cleaner flow
@gentamura
Copy link
Contributor Author

@satococoa review, please 🙏

@gentamura gentamura requested a review from satococoa February 6, 2025 04:53
Copy link
Contributor

@satococoa satococoa left a comment

Choose a reason for hiding this comment

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

Thank you!
Now it's very clear and straightforward! 👍

Comment on lines 11 to 19
const subscription = await stripe.subscriptions.retrieve(
invoice.subscription,
);

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

await stripe.invoices.pay(invoice.id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a comment explaining why we need to call pay when a subscription is canceled?

For example:
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added 0ff5641 👍

@gentamura
Copy link
Contributor Author

@shige @toyamarinyon review, please 🙏

Copy link
Member

@shige shige left a comment

Choose a reason for hiding this comment

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

LGTM! 💳

@gentamura
Copy link
Contributor Author

Thank you for reviewers 🚀

@gentamura gentamura merged commit 6042e96 into main Feb 7, 2025
9 checks passed
@gentamura gentamura deleted the feat/charge-last-month-usage-on-subscription-deletion branch February 7, 2025 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants