fix(premium): drop the grace period after a voluntary cancel (#3071)

This commit is contained in:
Hampus
2026-09-30 21:03:25 +02:00
committed by GitHub
parent 5e0806f479
commit 6e2f90b03c
38 changed files with 384 additions and 387 deletions
@@ -30,7 +30,7 @@ import type {StripePremiumService} from '@app/api/stripe/services/StripePremiumS
import type {StripeSubscriptionReconciler} from '@app/api/stripe/services/StripeSubscriptionReconciler';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository';
import {getPremiumPaymentRecoveryGraceMs, PREMIUM_GRACE_PERIOD_MS} from '@app/api/user/UserHelpers';
import {getPremiumPaymentRecoveryGraceMs} from '@app/api/user/UserHelpers';
import {mapUserToPrivateResponse} from '@app/api/user/UserMappers';
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError';
@@ -674,11 +674,11 @@ export class StripeSubscriptionWebhookHandler {
updates.premium_grace_ends_at = new Date(
lapseStart.getTime() + getPremiumPaymentRecoveryGraceMs(billingCycle),
);
} else if (cancelledBeforePeriodEnd) {
updates.premium_until = subscriptionEndedAt;
updates.premium_grace_ends_at = subscriptionEndedAt;
} else {
updates.premium_grace_ends_at = new Date(subscriptionEndedAt.getTime() + PREMIUM_GRACE_PERIOD_MS);
if (cancelledBeforePeriodEnd) {
updates.premium_until = subscriptionEndedAt;
}
updates.premium_grace_ends_at = subscriptionEndedAt;
}
}
}
@@ -577,7 +577,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => {
const {checkHasActivePaidPremium} = await import('@app/api/user/UserHelpers');
expect(checkHasActivePaidPremium(afterUser!)).toBe(false);
});
test('grants standard grace when the subscription is cancelled at the end of its paid period', async () => {
test('ends premium without grace when the subscription is cancelled at the end of its paid period', async () => {
const account = await createTestAccount(harness);
const userId = createUserID(BigInt(account.userId));
const subscriptionId = 'sub_test_cancel_natural';
@@ -599,10 +599,9 @@ describe('Stripe Webhook Subscription Lifecycle', () => {
expect(result.received).toBe(true);
const afterUser = await userRepository.findUnique(userId);
expect(afterUser?.premiumUntil?.getTime()).toBe(premiumUntil.getTime());
expect(afterUser?.premiumGraceEndsAt).not.toBeNull();
expect(afterUser!.premiumGraceEndsAt!.getTime()).toBe(endedAt * 1000 + PREMIUM_GRACE_PERIOD_MS);
expect(afterUser?.premiumGraceEndsAt?.getTime()).toBe(endedAt * 1000);
const {checkHasActivePaidPremium} = await import('@app/api/user/UserHelpers');
expect(checkHasActivePaidPremium(afterUser!)).toBe(true);
expect(checkHasActivePaidPremium(afterUser!)).toBe(false);
});
test('keeps the payment recovery deadline when Stripe cancels for non-payment', async () => {
const account = await createTestAccount(harness);