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:
2026-08-18 18:55:41 +03:00
parent bf46860feb
commit 60c208b3b3
7 changed files with 282 additions and 22 deletions
+23 -1
View File
@@ -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