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
8 changes: 5 additions & 3 deletions app/[locale]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ export default async function DashboardPage({
}

await expirePastScheduledSessionsForUser(user.id);
await ensureInitialTestSessions(user, accessState.policy);
await ensureInitialTestSessions(user, accessState.policy, {
skipExpiration: true,
});

const [sessionsData, performanceData] = await Promise.all([
const [sessionsData, performanceData, planNextAccess] = await Promise.all([
getDashboardSessionsData(user),
getDashboardPerformanceSummaryData(user.id),
getPlanNextAccess(user.id, accessState.policy),
]);
const planNextAccess = await getPlanNextAccess(user.id, accessState.policy);

const canJoinSessions = hasUserTierCapability(accessState, 'canJoinSessions');
const canCreateSession = hasUserTierCapability(
Expand Down
12 changes: 9 additions & 3 deletions lib/demo/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,14 @@ async function getDashboardCore(userId: string) {
).flatMap((result) => result.data ?? [])
: [];
const answeredCountBySession = new Map<string, Set<string>>();
const questionCountBySession = new Map<string, number>();

for (const sessionId of questionSessionById.values()) {
questionCountBySession.set(
sessionId,
(questionCountBySession.get(sessionId) ?? 0) + 1,
);
}

for (const answer of answeredQuestionIds) {
const questionId = answer.question_id;
Expand All @@ -1248,9 +1256,7 @@ async function getDashboardCore(userId: string) {
for (const session of sessions) {
session.questionCount = Math.max(
session.questionCount ?? 0,
[...questionSessionById.values()].filter(
(sessionId) => sessionId === session.id,
).length,
questionCountBySession.get(session.id) ?? 0,
);
session.answeredQuestionCount =
answeredCountBySession.get(session.id)?.size ??
Expand Down
16 changes: 7 additions & 9 deletions lib/session/expired-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,39 +85,37 @@ export async function expirePastScheduledSessionsForGroups(
Date.now() - SESSION_START_GRACE_MS,
).toISOString();

const { data: expiredUnplanned, error: unplannedError } = await admin
const { count: expiredUnplannedCount, error: unplannedError } = await admin
.schema('public')
.from('sessions')
.update({ status: 'expired' })
.update({ status: 'expired' }, { count: 'exact' })
.in('group_id', uniqueGroupIds)
.eq('status', 'scheduled')
.is('meeting_link', null)
.lt('scheduled_at', scheduledDayCutoff)
.select('id');
.lt('scheduled_at', scheduledDayCutoff);

if (unplannedError) {
console.error('[sessions] failed to expire past scheduled sessions', {
error: unplannedError,
});
}

const { data: expiredPlanned, error: plannedError } = await admin
const { count: expiredPlannedCount, error: plannedError } = await admin
.schema('public')
.from('sessions')
.update({ status: 'expired' })
.update({ status: 'expired' }, { count: 'exact' })
.in('group_id', uniqueGroupIds)
.eq('status', 'scheduled')
.not('meeting_link', 'is', null)
.lt('scheduled_at', plannedTimeCutoff)
.select('id');
.lt('scheduled_at', plannedTimeCutoff);

if (plannedError) {
console.error('[sessions] failed to expire missed planned sessions', {
error: plannedError,
});
}

return (expiredUnplanned?.length ?? 0) + (expiredPlanned?.length ?? 0);
return (expiredUnplannedCount ?? 0) + (expiredPlannedCount ?? 0);
}

export async function expirePastScheduledSession(sessionId: string) {
Expand Down
43 changes: 19 additions & 24 deletions lib/session/initial-test-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ const TEST_SESSION_QUESTION_GOAL = 20;
type AdminClient = ReturnType<typeof createSupabaseAdminClient>;

type UserRow = { id: string };
type TestSessionSlotRow = { name: string | null };
type TestSessionSlotRow = { name: string | null; status: string | null };
type EnsureInitialTestSessionsOptions = {
replaceExpired?: boolean;
skipExpiration?: boolean;
};

export async function ensureInitialTestSessions(
user: UserRow,
policy: Pick<AppPolicySettings, 'perQuestionTimerDefaultSeconds'>,
options: { replaceExpired?: boolean } = {},
options: EnsureInitialTestSessionsOptions = {},
) {
const admin = createSupabaseAdminClient();
const groupId = await getOrCreateTestGroup(admin, user);
await expirePastScheduledSessionsForGroups(admin, [groupId]);
const [existingSessions, totalCount] = await Promise.all([
listExistingTestWindowSessions(admin, groupId),
countAllTestSessions(admin, groupId),
]);
if (!options.skipExpiration) {
await expirePastScheduledSessionsForGroups(admin, [groupId]);
}

const testSessions = await listTestSessions(admin, groupId);
const existingSessions = testSessions.filter(
(session) => session.status !== 'cancelled' && session.status !== 'expired',
);
const totalCount = testSessions.length;
const existingCount = Math.min(existingSessions.length, TEST_SESSION_TARGET);
const missingCount = TEST_SESSION_TARGET - existingCount;

Expand Down Expand Up @@ -67,26 +76,12 @@ export async function ensureInitialTestSessions(
}
}

async function countAllTestSessions(admin: AdminClient, groupId: string) {
const { count } = await admin
.schema('public')
.from('sessions')
.select('id', { count: 'exact', head: true })
.eq('group_id', groupId);

return count ?? 0;
}

async function listExistingTestWindowSessions(
admin: AdminClient,
groupId: string,
) {
async function listTestSessions(admin: AdminClient, groupId: string) {
const { data } = await admin
.schema('public')
.from('sessions')
.select('name')
.eq('group_id', groupId)
.not('status', 'in', '("cancelled","expired")');
.select('name, status')
.eq('group_id', groupId);

return (data ?? []) as TestSessionSlotRow[];
}
Expand Down
14 changes: 13 additions & 1 deletion tests/trial-availability-and-session-flow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const onboardingActions = readFileSync(
'app/[locale]/onboarding/actions.ts',
'utf8',
);
const dashboardPage = readFileSync('app/[locale]/dashboard/page.tsx', 'utf8');
const dashboardData = readFileSync('lib/demo/data.ts', 'utf8');
const expiredSessionsHelper = readFileSync(
'lib/session/expired-sessions.ts',
'utf8',
Expand Down Expand Up @@ -450,13 +452,23 @@ test('past scheduled test sessions expire and require availability refresh for r
expiredSessionsHelper,
/\.not\('meeting_link', 'is', null\)\s+\.lt\('scheduled_at', plannedTimeCutoff\)/,
);
assert.match(
expiredSessionsHelper,
/update\(\{ status: 'expired' \}, \{ count: 'exact' \}\)/,
);
assert.match(
initialTestSessions,
/totalCount > 0 && !options\.replaceExpired/,
);
assert.match(
initialTestSessions,
/\.not\('status', 'in', '\("cancelled","expired"\)'\)/,
/session\.status !== 'cancelled' && session\.status !== 'expired'/,
);
assert.match(initialTestSessions, /skipExpiration\?: boolean/);
assert.match(dashboardPage, /skipExpiration: true/);
assert.match(
dashboardData,
/const questionCountBySession = new Map<string, number>\(\)/,
);
assert.match(initialTestSessions, /getOccupiedTestSessionNumbers/);
assert.match(initialTestSessions, /missingSessionNumbers/);
Expand Down
Loading