diff --git a/app/[locale]/dashboard/page.tsx b/app/[locale]/dashboard/page.tsx index 5944ad1..3de00bf 100644 --- a/app/[locale]/dashboard/page.tsx +++ b/app/[locale]/dashboard/page.tsx @@ -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( diff --git a/lib/demo/data.ts b/lib/demo/data.ts index f7f8f38..5811106 100644 --- a/lib/demo/data.ts +++ b/lib/demo/data.ts @@ -1232,6 +1232,14 @@ async function getDashboardCore(userId: string) { ).flatMap((result) => result.data ?? []) : []; const answeredCountBySession = new Map>(); + const questionCountBySession = new Map(); + + for (const sessionId of questionSessionById.values()) { + questionCountBySession.set( + sessionId, + (questionCountBySession.get(sessionId) ?? 0) + 1, + ); + } for (const answer of answeredQuestionIds) { const questionId = answer.question_id; @@ -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 ?? diff --git a/lib/session/expired-sessions.ts b/lib/session/expired-sessions.ts index 3e2f310..8eac8fd 100644 --- a/lib/session/expired-sessions.ts +++ b/lib/session/expired-sessions.ts @@ -85,15 +85,14 @@ 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', { @@ -101,15 +100,14 @@ export async function expirePastScheduledSessionsForGroups( }); } - 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', { @@ -117,7 +115,7 @@ export async function expirePastScheduledSessionsForGroups( }); } - return (expiredUnplanned?.length ?? 0) + (expiredPlanned?.length ?? 0); + return (expiredUnplannedCount ?? 0) + (expiredPlannedCount ?? 0); } export async function expirePastScheduledSession(sessionId: string) { diff --git a/lib/session/initial-test-sessions.ts b/lib/session/initial-test-sessions.ts index 8fc8e1c..7ea4637 100644 --- a/lib/session/initial-test-sessions.ts +++ b/lib/session/initial-test-sessions.ts @@ -12,19 +12,28 @@ const TEST_SESSION_QUESTION_GOAL = 20; type AdminClient = ReturnType; 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, - 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; @@ -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[]; } diff --git a/tests/trial-availability-and-session-flow.test.mjs b/tests/trial-availability-and-session-flow.test.mjs index 28d4435..910db25 100644 --- a/tests/trial-availability-and-session-flow.test.mjs +++ b/tests/trial-availability-and-session-flow.test.mjs @@ -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', @@ -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\(\)/, ); assert.match(initialTestSessions, /getOccupiedTestSessionNumbers/); assert.match(initialTestSessions, /missingSessionNumbers/);