seubert-claims/ondeck/src/app/api/cron/auto-generate/route.ts
lorentz 0abcadaa71 Feature: policy groups, task assignment, claims advocate, client members, audit log, combobox UI
- Schema: add PolicyGroup, ClientMember, claimsAdvocateId; migrations included
- API: /api/clients/[id]/team (flat ClientMember), /api/clients/[id]/policy-groups
- API: /api/policy-groups, /api/tasks/bulk-assign, /api/cron/auto-generate
- API: /api/admin/audit, filter clients by advocateId/teamMemberId (name format fix)
- API: /api/users supports department filter
- UI: policy-group-manager, client-detail assignments card (combobox, claims dept filter)
- UI: bulk assign page /tasks/assign, audit log viewer /admin/audit
- UI: nav-bar Assign Tasks link, admin audit log link
- UI: combobox component with search/filter
- Auth: audit logging for sign-in, user role changes
- Fix: Prisma client regenerated for new schema fields
- Fix: teamMemberId filter uses Last,First name format for policy matching
- Fix: suppressHydrationWarning on all formatDate spans
2026-03-18 01:49:52 +00:00

237 lines
7.7 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
const TWENTY_DAYS_MS = 20 * 24 * 60 * 60 * 1000
/**
* POST /api/cron/auto-generate
* Called by a cron scheduler. Protected by CRON_SECRET header.
*
* Rules:
* - Policy must be >= 20 days old in the system
* - If policy is in a group, use group renewalDate; otherwise use policy expirationDate
* - Skip policies/groups that already have tasks generated from templates
* - Auto-assign to client's claimsAdvocate if set
*/
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-cron-secret')
if (!secret || secret !== process.env.CRON_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const killSwitch = await prisma.syncConfig.findUnique({
where: { key: 'task_auto_generate_enabled' },
})
if (killSwitch?.value === 'false') {
return NextResponse.json({ message: 'Auto-generate disabled via SyncConfig' })
}
const cutoff = new Date(Date.now() - TWENTY_DAYS_MS)
let groupTasksCreated = 0
let policyTasksCreated = 0
let errors: string[] = []
try {
// ─── 1. Policy Groups ───────────────────────────────────────────────────
// Find groups whose policies are all >= 20 days old and have no template-generated tasks yet
const groups = await prisma.policyGroup.findMany({
where: {
policies: {
every: { createdAt: { lte: cutoff } },
some: {}, // group must have at least one policy
},
tasks: {
none: { templateId: { not: null } },
},
},
include: {
client: {
select: {
designationId: true,
designation2Id: true,
claimsAdvocateId: true,
},
},
},
})
for (const group of groups) {
try {
const designationIds = [
group.client.designationId,
group.client.designation2Id,
].filter(Boolean) as string[]
const templates = await prisma.taskTemplate.findMany({
where: {
isActive: true,
OR: [
{ designationId: null },
...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []),
],
},
orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }],
})
if (templates.length === 0) continue
const renewalDate = new Date(group.renewalDate)
const tasksToCreate = templates.map((template) => {
const dueDate = new Date(renewalDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
return {
title: template.name,
description: template.description,
department: template.department,
timing: template.timing,
daysOffset: template.daysOffset,
dueDate,
status: 'NOT_STARTED' as const,
priority: template.defaultPriority,
clientId: group.clientId,
policyGroupId: group.id,
templateId: template.id,
}
})
const created = await prisma.task.createMany({ data: tasksToCreate })
groupTasksCreated += created.count
// Auto-assign to claims advocate
if (group.client.claimsAdvocateId && created.count > 0) {
const newTasks = await prisma.task.findMany({
where: { policyGroupId: group.id, templateId: { in: templates.map((t) => t.id) } },
select: { id: true },
})
if (newTasks.length > 0) {
await prisma.taskAssignment.createMany({
data: newTasks.map((t) => ({
taskId: t.id,
userId: group.client.claimsAdvocateId!,
})),
skipDuplicates: true,
})
}
}
await prisma.auditLog.create({
data: {
action: 'AUTO_GENERATE_TASKS_GROUP',
entityType: 'PolicyGroup',
entityId: group.id,
newValues: { tasksCreated: created.count, renewalDate: group.renewalDate },
},
})
} catch (err: any) {
errors.push(`Group ${group.id}: ${err.message}`)
}
}
// ─── 2. Individual Policies (not in a group) ────────────────────────────
const policies = await prisma.policy.findMany({
where: {
createdAt: { lte: cutoff },
policyGroupId: null,
tasks: {
none: { templateId: { not: null } },
},
},
include: {
client: {
select: {
designationId: true,
designation2Id: true,
claimsAdvocateId: true,
},
},
},
})
for (const policy of policies) {
try {
const designationIds = [
policy.client.designationId,
policy.client.designation2Id,
].filter(Boolean) as string[]
const templates = await prisma.taskTemplate.findMany({
where: {
isActive: true,
OR: [
{ designationId: null },
...(designationIds.length > 0 ? [{ designationId: { in: designationIds } }] : []),
],
},
orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }],
})
if (templates.length === 0) continue
const anchorDate = new Date(policy.expirationDate)
const tasksToCreate = templates.map((template) => {
const dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
return {
title: template.name,
description: template.description,
department: template.department,
timing: template.timing,
daysOffset: template.daysOffset,
dueDate,
status: 'NOT_STARTED' as const,
priority: template.defaultPriority,
clientId: policy.clientId,
policyId: policy.id,
templateId: template.id,
}
})
const created = await prisma.task.createMany({ data: tasksToCreate })
policyTasksCreated += created.count
// Auto-assign to claims advocate
if (policy.client.claimsAdvocateId && created.count > 0) {
const newTasks = await prisma.task.findMany({
where: { policyId: policy.id, templateId: { in: templates.map((t) => t.id) } },
select: { id: true },
})
if (newTasks.length > 0) {
await prisma.taskAssignment.createMany({
data: newTasks.map((t) => ({
taskId: t.id,
userId: policy.client.claimsAdvocateId!,
})),
skipDuplicates: true,
})
}
}
await prisma.auditLog.create({
data: {
action: 'AUTO_GENERATE_TASKS_POLICY',
entityType: 'Policy',
entityId: policy.id,
newValues: { tasksCreated: created.count, expirationDate: policy.expirationDate },
},
})
} catch (err: any) {
errors.push(`Policy ${policy.id}: ${err.message}`)
}
}
return NextResponse.json({
groupTasksCreated,
policyTasksCreated,
totalCreated: groupTasksCreated + policyTasksCreated,
groupsProcessed: groups.length,
policiesProcessed: policies.length,
errors: errors.length > 0 ? errors : undefined,
})
} catch (error: any) {
console.error('Auto-generate cron error:', error)
return NextResponse.json({ error: 'Internal server error', detail: error.message }, { status: 500 })
}
}