mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
fix(admin): show the cardless trial as a trial in the admin panel
The cardless trial writes trialEndsAt and nothing else, because there is no Stripe subscription behind it to report trialing. subscriptionStatus stays FREE, so every admin view reading that column alone showed a live trial as a free account: On Trial sat at zero, Free Users counted the trials, the user table badged them Free with an open-ended Active access, the Trialing filter returned nobody and the growth scoreboard left them out of the paid accounts table. Access has always been resolved from the date (hasBillingAccess), so the display now follows the same date through getEffectiveBillingStatus. Only FREE is overridden: any other status means Stripe has an opinion worth showing.
This commit is contained in:
@@ -162,7 +162,8 @@ describe('getScoreboard', () => {
|
||||
const busy = await createUser({ subscriptionStatus: 'ACTIVE' });
|
||||
const silent = await createUser({ subscriptionStatus: 'ACTIVE' });
|
||||
const trialing = await createUser({ subscriptionStatus: 'TRIALING' });
|
||||
await createUser({ subscriptionStatus: 'FREE' });
|
||||
// Free with nothing left to run, so it stays out of the table.
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: null });
|
||||
|
||||
await seedEvent({ name: 'VIDEO_ADDED', occurredAt: daysAgo(2), userId: busy.id });
|
||||
await seedEvent({ name: 'SHARE_LINK_CREATED', occurredAt: daysAgo(20), userId: busy.id });
|
||||
@@ -185,6 +186,27 @@ describe('getScoreboard', () => {
|
||||
expect(busyRow?.valueEvents7).toBe(1);
|
||||
expect(busyRow?.valueEvents30).toBe(2);
|
||||
});
|
||||
|
||||
// A cardless trial has no Stripe subscription to hold the status, so it sits at
|
||||
// FREE with only a date behind it. Filtering on the status column alone left
|
||||
// every trial account out of this table and out of the at-risk list with it.
|
||||
it('includes a cardless trial and reports it as trialing', async () => {
|
||||
const cardless = await createUser({
|
||||
subscriptionStatus: 'FREE',
|
||||
trialEndsAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||
});
|
||||
const expired = await createUser({
|
||||
subscriptionStatus: 'FREE',
|
||||
trialEndsAt: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
});
|
||||
|
||||
const scoreboard = await getScoreboard({ weeks: 4 });
|
||||
const ids = scoreboard.paidAccounts.map((row) => row.userId);
|
||||
|
||||
expect(ids).toEqual([cardless.id]);
|
||||
expect(ids).not.toContain(expired.id);
|
||||
expect(scoreboard.paidAccounts[0]?.status).toBe('TRIALING');
|
||||
});
|
||||
});
|
||||
|
||||
// The cohort comparison is another block of raw SQL, and the part most easily
|
||||
|
||||
@@ -869,9 +869,11 @@ describe('getCachedStripeStats', () => {
|
||||
await createUser({ subscriptionStatus: 'TRIALING' });
|
||||
await createUser({ subscriptionStatus: 'PAST_DUE' });
|
||||
await createUser({ subscriptionStatus: 'CANCELED' });
|
||||
await createUser({ subscriptionStatus: 'FREE' });
|
||||
await createUser({ subscriptionStatus: 'FREE' });
|
||||
await createUser({ subscriptionStatus: 'FREE' });
|
||||
// Free means free: no trial left to run, or these would be counted as the
|
||||
// cardless trials they would then be.
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: null });
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: null });
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: null });
|
||||
const stripe = stubStripePrice({ unit_amount: 1900, currency: 'eur' });
|
||||
|
||||
expect(await getCachedStripeStats()).toEqual({
|
||||
@@ -887,6 +889,45 @@ describe('getCachedStripeStats', () => {
|
||||
expect(stripe.retrievedPriceIds).toEqual(['price_admin_stats_test']);
|
||||
});
|
||||
|
||||
// The cardless trial writes only trialEndsAt, so the account sits at FREE with no
|
||||
// Stripe subscription behind it. Counting the status column alone reported every
|
||||
// one of them as a free user and left "On Trial" at zero on the dashboard.
|
||||
it('counts a cardless trial as trialing rather than as free', async () => {
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: new Date(Date.now() + 60_000) });
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: null });
|
||||
stubStripePrice({ unit_amount: 1900, currency: 'usd' });
|
||||
|
||||
const stats = await getCachedStripeStats();
|
||||
|
||||
expect(stats?.trialingUsers).toBe(1);
|
||||
expect(stats?.freeUsers).toBe(1);
|
||||
});
|
||||
|
||||
it('counts an expired trial back as a free user', async () => {
|
||||
await createUser({ subscriptionStatus: 'FREE', trialEndsAt: new Date(Date.now() - 60_000) });
|
||||
stubStripePrice({ unit_amount: 1900, currency: 'usd' });
|
||||
|
||||
const stats = await getCachedStripeStats();
|
||||
|
||||
expect(stats?.trialingUsers).toBe(0);
|
||||
expect(stats?.freeUsers).toBe(1);
|
||||
});
|
||||
|
||||
// A Stripe trial is already TRIALING and is not sitting in the FREE bucket, so it
|
||||
// must not be added on top of the cardless count.
|
||||
it('does not double count a Stripe trial that also carries a trial end date', async () => {
|
||||
await createUser({
|
||||
subscriptionStatus: 'TRIALING',
|
||||
trialEndsAt: new Date(Date.now() + 60_000),
|
||||
});
|
||||
stubStripePrice({ unit_amount: 1900, currency: 'usd' });
|
||||
|
||||
const stats = await getCachedStripeStats();
|
||||
|
||||
expect(stats?.trialingUsers).toBe(1);
|
||||
expect(stats?.freeUsers).toBe(0);
|
||||
});
|
||||
|
||||
// UNPAID, INCOMPLETE and INCOMPLETE_EXPIRED are real values of the enum that used to
|
||||
// belong to none of the reported buckets, so those users were counted nowhere and the
|
||||
// five totals silently did not add up to the user table. They must not be folded into
|
||||
|
||||
Reference in New Issue
Block a user