import { prisma } from '@/lib/prisma' import { applyAutoTagRules, aiInterpretCriteria, type AutoTagRule, } from './ai-award-eligibility' const BATCH_SIZE = 20 /** * Process eligibility for an award in the background. * Updates progress in the database as it goes so the frontend can poll. */ export async function processEligibilityJob( awardId: string, includeSubmitted: boolean, userId: string ): Promise { try { // Mark job as PROCESSING const award = await prisma.specialAward.findUniqueOrThrow({ where: { id: awardId }, include: { program: true }, }) // Get projects const statusFilter = includeSubmitted ? (['SUBMITTED', 'ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST'] as const) : (['ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST'] as const) const projects = await prisma.project.findMany({ where: { programId: award.programId, status: { in: [...statusFilter] }, }, select: { id: true, title: true, description: true, competitionCategory: true, country: true, geographicZone: true, tags: true, oceanIssue: true, }, }) if (projects.length === 0) { await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobStatus: 'COMPLETED', eligibilityJobTotal: 0, eligibilityJobDone: 0, }, }) return } await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobStatus: 'PROCESSING', eligibilityJobTotal: projects.length, eligibilityJobDone: 0, eligibilityJobError: null, eligibilityJobStarted: new Date(), }, }) // Phase 1: Auto-tag rules (deterministic, fast) const autoTagRules = award.autoTagRulesJson as unknown as AutoTagRule[] | null let autoResults: Map | undefined if (autoTagRules && Array.isArray(autoTagRules) && autoTagRules.length > 0) { autoResults = applyAutoTagRules(autoTagRules, projects) } // Phase 2: AI interpretation (if criteria text exists AND AI eligibility is enabled) // Process in batches to avoid timeouts let aiResults: Map | undefined if (award.criteriaText && award.useAiEligibility) { aiResults = new Map() for (let i = 0; i < projects.length; i += BATCH_SIZE) { const batch = projects.slice(i, i + BATCH_SIZE) const aiEvals = await aiInterpretCriteria(award.criteriaText, batch) for (const e of aiEvals) { aiResults.set(e.projectId, { eligible: e.eligible, confidence: e.confidence, reasoning: e.reasoning, }) } // Update progress await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobDone: Math.min(i + BATCH_SIZE, projects.length), }, }) } } else { // No AI needed, mark all as done await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobDone: projects.length }, }) } // Combine results: auto-tag AND AI must agree (or just one if only one configured) const eligibilities = projects.map((project) => { const autoEligible = autoResults?.get(project.id) ?? true const aiEval = aiResults?.get(project.id) const aiEligible = aiEval?.eligible ?? true const eligible = autoEligible && aiEligible const method = autoResults && aiResults ? 'AUTO' : autoResults ? 'AUTO' : 'MANUAL' return { projectId: project.id, eligible, method, aiReasoningJson: aiEval ? { confidence: aiEval.confidence, reasoning: aiEval.reasoning } : null, } }) // Upsert eligibilities await prisma.$transaction( eligibilities.map((e) => prisma.awardEligibility.upsert({ where: { awardId_projectId: { awardId, projectId: e.projectId, }, }, create: { awardId, projectId: e.projectId, eligible: e.eligible, method: e.method as 'AUTO' | 'MANUAL', aiReasoningJson: e.aiReasoningJson ?? undefined, }, update: { eligible: e.eligible, method: e.method as 'AUTO' | 'MANUAL', aiReasoningJson: e.aiReasoningJson ?? undefined, overriddenBy: null, overriddenAt: null, }, }) ) ) // Mark as completed await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobStatus: 'COMPLETED', eligibilityJobDone: projects.length, }, }) } catch (error) { // Mark as failed const errorMessage = error instanceof Error ? error.message : 'Unknown error' try { await prisma.specialAward.update({ where: { id: awardId }, data: { eligibilityJobStatus: 'FAILED', eligibilityJobError: errorMessage, }, }) } catch { // If we can't even update the status, log and give up console.error('Failed to update eligibility job status:', error) } } }