Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,11 @@ export class Billing<P extends string = string> {
ended_at: sub.ended_at === null ? null : instant(sub.ended_at),
period_end: instant(item.current_period_end),
trial_end: sub.trial_end === null ? null : instant(sub.trial_end),
cancel_at_period_end: sub.cancel_at_period_end,
// The customer portal schedules its period-end cancellation through
// `cancel_at`, leaving `cancel_at_period_end` false.
cancel_at_period_end:
sub.cancel_at_period_end ||
(sub.cancel_at !== null && sub.cancel_at <= item.current_period_end),
updated_at: now,
};
});
Expand Down
10 changes: 9 additions & 1 deletion packages/stripe/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export async function createStripeDev(
ended_at: null,
canceled_at: null,
trial_end: days ? seconds() + days * 86400 : null,
cancel_at: null,
cancel_at_period_end: false,
metadata: Object.fromEntries(
[...p]
Expand Down Expand Up @@ -207,14 +208,21 @@ export async function createStripeDev(
}
function transition(
subscriptionId: string,
action: 'renew' | 'payment-failed' | 'cancel' | 'cancel-at-period-end',
action:
| 'renew'
| 'payment-failed'
| 'cancel'
| 'cancel-at-period-end'
| 'portal-cancel',
) {
const sub = subscriptions.get(subscriptionId);
if (!sub) throw new Error('Subscription not found');
let result: Stripe.Event;
if (action === 'cancel') result = cancel(sub);
else {
if (action === 'cancel-at-period-end') sub.cancel_at_period_end = true;
else if (action === 'portal-cancel')
sub.cancel_at = sub.items.data[0]!.current_period_end;
else {
sub.status = action === 'renew' ? 'active' : 'past_due';
sub.items.data[0]!.current_period_end = periodEnd(
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/billing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,26 @@ billingTest(
},
);

billingTest(
'a period-end cancellation from the customer portal reads as not renewing',
async ({ f }) => {
const { session } = await f.start('monthly');
const sub = f.dev.completeCheckout(session.id);
await f.deliver();
const row = () =>
f.billing.db
.selectFrom('subscriptions')
.select('cancel_at_period_end')
.where('id', '=', sub.id)
.executeTakeFirstOrThrow();
expect((await row()).cancel_at_period_end).toBe(false);
f.dev.transition(sub.id, 'portal-cancel');
await f.deliver();
expect((await row()).cancel_at_period_end).toBe(true);
expect((await f.billing.status('alice')).access).toBe(true);
},
);

billingTest(
'yearly discount uses the configured price; canceled accounts cannot repeat a trial',
async ({ f }) => {
Expand Down
Loading