import { prisma } from '../config/database'; import { logger } from '../utils/logger'; import type { ChallengeMetric } from '@prisma/client'; async function computeScore( userId: string, metric: ChallengeMetric, startsAt: Date, endsAt: Date, ): Promise { const between = { gte: startsAt, lte: endsAt }; switch (metric) { case 'DOORS_KNOCKED': return prisma.canvassVisit.count({ where: { session: { userId }, visitedAt: between, }, }); case 'EMAILS_SENT': return prisma.campaignEmail.count({ where: { userId, sentAt: between, }, }); case 'SHIFTS_ATTENDED': return prisma.shiftSignup.count({ where: { userId, status: 'CONFIRMED', shift: { startTime: { gte: startsAt.toISOString(), lte: endsAt.toISOString() } }, }, }); case 'RESPONSES_SUBMITTED': return prisma.representativeResponse.count({ where: { submittedByUserId: userId, createdAt: between, }, }); case 'REFERRALS_MADE': return prisma.referral.count({ where: { referrerId: userId, completedAt: between, }, }); default: return 0; } } async function scoreChallenge(challengeId: string): Promise { const challenge = await prisma.challenge.findUnique({ where: { id: challengeId }, include: { teams: { include: { members: { select: { id: true, userId: true, score: true } }, }, }, }, }); if (!challenge) return; let anyChanged = false; const participantUserIds: string[] = []; for (const team of challenge.teams) { let teamTotal = 0; for (const member of team.members) { const newScore = await computeScore( member.userId, challenge.metric, challenge.startsAt, challenge.endsAt, ); if (newScore !== member.score) { await prisma.challengeTeamMember.update({ where: { id: member.id }, data: { score: newScore }, }); anyChanged = true; } teamTotal += newScore; participantUserIds.push(member.userId); } if (teamTotal !== team.score) { await prisma.challengeTeam.update({ where: { id: team.id }, data: { score: teamTotal, lastScoredAt: new Date() }, }); anyChanged = true; } } if (anyChanged && participantUserIds.length > 0) { try { const { sseService } = await import('../modules/social/sse.service'); sseService.sendToUsers(participantUserIds, 'challenge_scores_updated', { challengeId, }); } catch { // SSE not available } } } async function processLifecycle(): Promise { const now = new Date(); // UPCOMING -> ACTIVE const toActivate = await prisma.challenge.findMany({ where: { status: 'UPCOMING', startsAt: { lte: now } }, }); for (const c of toActivate) { await prisma.challenge.update({ where: { id: c.id }, data: { status: 'ACTIVE' }, }); logger.info(`Challenge ${c.id} "${c.title}" activated`); } // ACTIVE -> COMPLETED (past end date) const toComplete = await prisma.challenge.findMany({ where: { status: 'ACTIVE', endsAt: { lte: now } }, }); for (const c of toComplete) { await scoreChallenge(c.id); await prisma.challenge.update({ where: { id: c.id }, data: { status: 'COMPLETED' }, }); logger.info(`Challenge ${c.id} "${c.title}" completed`); } // Score remaining ACTIVE challenges const active = await prisma.challenge.findMany({ where: { status: 'ACTIVE' }, select: { id: true }, }); for (const c of active) { await scoreChallenge(c.id); } logger.info( `Challenge lifecycle: ${toActivate.length} activated, ${toComplete.length} completed, ${active.length} active scored`, ); } export const challengeScoringService = { computeScore, scoreChallenge, processLifecycle, };