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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

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 finalizeAndPayInvoice function for proper invoice state transitions
  • Added subscription data synchronisation after cancellation
  • Improved test environment support with STRIPE_TEST_CLOCK_ENABLED flag
  • Refactored webhook handler for better error handling and code organisation

Testing

  • Subscription cancellation flow tested
  • Verified correct invoice state transitions (draft → open → 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 Jan 9, 2025 4:28am

… 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.

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 to use the simulation function on the Stripe admin panel. We wondered whether to make it an environment variable, but decided to define it within a function as it is not a constant that is used in multiple places.


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.

async function finalizeAndPayInvoice(invoiceId: string) {
try {
await stripe.invoices.finalizeInvoice(invoiceId);
await stripe.invoices.pay(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 Open to Paid with this method.

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

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
Comment on lines 94 to 97
// TODO: This block will be removed in the other issue.
if (event.data.object.billing_reason === "subscription_cycle") {
await handleSubscriptionCycleInvoice(event.data.object);
}
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.

📝 This block will be removed in the other issue. We plan to add user seats in other ways.

ref: #294

@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.

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.

2 participants