-
Notifications
You must be signed in to change notification settings - Fork 16
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
feat(billing): Implement last month usage charging on subscription cancellation #296
Conversation
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.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
… 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.
d79ae15
to
edfa1aa
Compare
@@ -82,33 +83,20 @@ export async function POST(req: Request) { | |||
); | |||
} | |||
await handleSubscriptionCancellation(event.data.object); | |||
await upsertSubscription(event.data.object.id); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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.
a10f8f9
to
6ddee1e
Compare
@satococoa review, please 🙏 |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@satococoa review, please 🙏 |
There was a problem hiding this 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! 👍
const subscription = await stripe.subscriptions.retrieve( | ||
invoice.subscription, | ||
); | ||
|
||
if (subscription.status !== "canceled") { | ||
return; | ||
} | ||
|
||
await stripe.invoices.pay(invoice.id); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 0ff5641 👍
@shige @toyamarinyon review, please 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! 💳
Thank you for reviewers 🚀 |
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
Testing
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.