fix(billing): stop an unpaid subscription from unlocking the paid limits

isPaidTier read a future stripeCurrentPeriodEnd as proof of payment, and
Stripe stamps a current period on an incomplete subscription all the
same. A checkout whose first charge failed therefore carried a period
end a month out with nothing paid behind it, and every ceiling the
cardless trial puts on an unpaid account (200 GB of storage, unlimited
projects, unlimited workspaces) came off with it.

Written as a deny list of the two statuses that mean no charge has ever
gone through, so a real customer whose renewal failed keeps the full
plan for the period they already paid for.

hasBillingAccess reads the same column the same way and is deliberately
left alone: being wrong there locks a paying customer out, and the SQL
in buildBillingAccessWhereInput has to move with it.
This commit is contained in:
2026-08-18 09:48:10 +03:00
parent 1ec5c53802
commit 313cd552e6
2 changed files with 60 additions and 0 deletions
+42
View File
@@ -185,6 +185,48 @@ describe('isPaidTier', () => {
).toBe(false);
});
// A checkout whose first charge never went through. Stripe hands back a
// subscription carrying a period end a month out, and reading that as payment
// would have given a free account the full 200 GB and unlimited projects.
it('does not count an incomplete subscription as paid, period end or not', () => {
expect(
isPaidTier(
{
subscriptionStatus: BillingSubscriptionStatus.INCOMPLETE,
stripeCurrentPeriodEnd: new Date(NOW.getTime() + 30 * DAY_MS),
},
NOW
)
).toBe(false);
});
it('does not count an expired incomplete subscription as paid', () => {
expect(
isPaidTier(
{
subscriptionStatus: BillingSubscriptionStatus.INCOMPLETE_EXPIRED,
stripeCurrentPeriodEnd: new Date(NOW.getTime() + 30 * DAY_MS),
},
NOW
)
).toBe(false);
});
// A card that failed on renewal is a customer, not a free account: the period
// it is inside was paid for. Kept as a test because the fix above is one
// `Set.has` away from catching this case too.
it('still counts a past due subscription inside its paid period as paid', () => {
expect(
isPaidTier(
{
subscriptionStatus: BillingSubscriptionStatus.PAST_DUE,
stripeCurrentPeriodEnd: new Date(NOW.getTime() + DAY_MS),
},
NOW
)
).toBe(true);
});
it('does not count an expired paid period as paid', () => {
expect(
isPaidTier(