From 64ea7800ed07dfb2dbe6800e276f3398c94de17f Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 17 Jul 2026 20:59:02 +0000 Subject: [PATCH] fix(tasks): skip generating tasks with computed due dates in the past auto-generate.ts and all three manual task-generation routes computed due dates purely from a policy/group anchor date plus a template's daysOffset, with no check against the current date. Surfaced when a manual backfill for two clients created tasks anchored to an already- expired policy term, producing due dates back in 2024. Add isRelevantDueDate() and apply it at every task-creation call site so no template-generated task is ever born already overdue. Co-Authored-By: Claude Sonnet 5 --- .../[id]/generate-policy-tasks/route.ts | 2 + .../[id]/generate-tasks/route.ts | 60 +++++++------- .../api/tasks/generate-and-assign/route.ts | 80 ++++++++++--------- .../lib/sync/__tests__/auto-generate.test.ts | 68 ++++++++++++++++ ondeck/src/lib/sync/auto-generate.ts | 74 +++++++++-------- ondeck/src/lib/sync/task-due-date.ts | 11 +++ 6 files changed, 198 insertions(+), 97 deletions(-) create mode 100644 ondeck/src/lib/sync/task-due-date.ts diff --git a/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts b/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts index ffedb60..f437a28 100644 --- a/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts +++ b/ondeck/src/app/api/policy-groups/[id]/generate-policy-tasks/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' +import { isRelevantDueDate } from '@/lib/sync/task-due-date' /** * POST /api/policy-groups/[id]/generate-policy-tasks @@ -89,6 +90,7 @@ export async function POST( createdBy: (session.user as any).id, } }) + .filter((task) => isRelevantDueDate(task.dueDate)) if (tasksToCreate.length === 0) { return NextResponse.json({ created: 0 }) diff --git a/ondeck/src/app/api/policy-groups/[id]/generate-tasks/route.ts b/ondeck/src/app/api/policy-groups/[id]/generate-tasks/route.ts index 6de01af..d961a82 100644 --- a/ondeck/src/app/api/policy-groups/[id]/generate-tasks/route.ts +++ b/ondeck/src/app/api/policy-groups/[id]/generate-tasks/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' +import { isRelevantDueDate } from '@/lib/sync/task-due-date' /** * POST /api/policy-groups/[id]/generate-tasks @@ -78,26 +79,36 @@ export async function POST( const renewalDate = new Date(group.renewalDate) - const tasksToCreate = templates.map((template) => { - const dueDate = new Date(renewalDate) - dueDate.setDate(dueDate.getDate() + template.daysOffset) + 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, - taskGroup: template.taskGroup, - clientId: group.clientId, - policyGroupId: group.id, - templateId: template.id, - createdBy: (session.user as any).id, - } - }) + 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, + taskGroup: template.taskGroup, + clientId: group.clientId, + policyGroupId: group.id, + templateId: template.id, + createdBy: (session.user as any).id, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + + if (tasksToCreate.length === 0) { + return NextResponse.json({ + created: 0, + skipped: alreadyGeneratedTemplateIds.length, + message: 'No new templates produced a relevant (today-or-later) due date', + }) + } const result = await prisma.task.createMany({ data: tasksToCreate as any[], @@ -111,7 +122,7 @@ export async function POST( entityId: group.id, newValues: { tasksCreated: result.count, - templatesUsed: templates.map((t) => t.name), + templatesUsed: tasksToCreate.map((t) => t.title), renewalDate: group.renewalDate, }, }, @@ -120,14 +131,7 @@ export async function POST( return NextResponse.json({ created: result.count, skipped: alreadyGeneratedTemplateIds.length, - templates: templates.map((t) => ({ - name: t.name, - dueDate: new Date( - new Date(group.renewalDate).setDate( - new Date(group.renewalDate).getDate() + t.daysOffset - ) - ), - })), + templates: tasksToCreate.map((t) => ({ name: t.title, dueDate: t.dueDate })), }) } catch (error) { console.error('Generate tasks error:', error) diff --git a/ondeck/src/app/api/tasks/generate-and-assign/route.ts b/ondeck/src/app/api/tasks/generate-and-assign/route.ts index 34a9880..2e8594c 100644 --- a/ondeck/src/app/api/tasks/generate-and-assign/route.ts +++ b/ondeck/src/app/api/tasks/generate-and-assign/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/db' +import { isRelevantDueDate } from '@/lib/sync/task-due-date' /** * POST /api/tasks/generate-and-assign @@ -96,25 +97,28 @@ export async function POST(request: NextRequest) { for (const group of groups) { const renewalDate = new Date(group.renewalDate) - const tasksToCreate = groupTemplates.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, - createdBy: (session.user as any).id, - } - }) + const tasksToCreate = groupTemplates + .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, + createdBy: (session.user as any).id, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) tasksCreated += created.count @@ -146,25 +150,28 @@ export async function POST(request: NextRequest) { for (const policy of policies) { const anchorDate = new Date(policy.expirationDate) anchorDate.setDate(anchorDate.getDate() + 1) // renewal date = expiration + 1 - const tasksToCreate = policyTemplates.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, - createdBy: (session.user as any).id, - } - }) + const tasksToCreate = policyTemplates + .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, + createdBy: (session.user as any).id, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) tasksCreated += created.count @@ -219,6 +226,7 @@ export async function POST(request: NextRequest) { const dueDate = new Date(anchorDate) dueDate.setDate(dueDate.getDate() + template.daysOffset) + if (!isRelevantDueDate(dueDate)) continue const created = await prisma.task.create({ data: { diff --git a/ondeck/src/lib/sync/__tests__/auto-generate.test.ts b/ondeck/src/lib/sync/__tests__/auto-generate.test.ts index eaf2732..f888c0f 100644 --- a/ondeck/src/lib/sync/__tests__/auto-generate.test.ts +++ b/ondeck/src/lib/sync/__tests__/auto-generate.test.ts @@ -129,3 +129,71 @@ describe('runAutoGenerate — CLIENT-level task generation', () => { expect(result.clientTasksCreated).toBe(1) }) }) + +describe('runAutoGenerate — past-due task filtering', () => { + it('does not create a POLICY-level task whose computed due date is already in the past', async () => { + // "now" is fixed at 2026-07-09. This policy's term ended over a year ago + // (2025-05-19), so every template anchored to it computes a due date well + // before "now" and must be skipped entirely. + const policy = { + id: 'policy-expired-term', + clientId: 'client-dagostino', + policyType: 'Package', + expirationDate: new Date('2025-05-19'), + client: { designationId: null, designation2Id: null, claimsAdvocateId: null }, + } + mockPolicyFindMany.mockResolvedValue([policy]) + mockTaskTemplateFindMany.mockResolvedValue([ + { + id: 'template-onboarding', + name: 'SHAPE Onboarding Checklist', + description: null, + department: 'Commercial Lines', + timing: 'PRE_RENEWAL', + daysOffset: -337, + defaultPriority: 'NORMAL', + level: 'POLICY', + policyTypeFilter: null, + }, + ]) + + const result = await runAutoGenerate(config) + + expect(mockTaskCreateMany).not.toHaveBeenCalled() + expect(result.policyTasksCreated).toBe(0) + }) + + it('still creates POLICY-level tasks whose computed due date is today or later, for the same policy', async () => { + const policy = { + id: 'policy-current-term', + clientId: 'client-sultan', + policyType: 'Package', + expirationDate: new Date('2027-06-10'), + client: { designationId: null, designation2Id: null, claimsAdvocateId: null }, + } + mockPolicyFindMany.mockResolvedValue([policy]) + mockTaskTemplateFindMany.mockResolvedValue([ + { + id: 'template-loss-runs', + name: 'Request 125 day loss runs', + description: null, + department: 'Commercial Lines', + timing: 'PRE_RENEWAL', + daysOffset: -125, + defaultPriority: 'NORMAL', + level: 'POLICY', + policyTypeFilter: null, + }, + ]) + mockTaskCreateMany.mockResolvedValue({ count: 1 }) + + const result = await runAutoGenerate(config) + + expect(mockTaskCreateMany).toHaveBeenCalledWith( + expect.objectContaining({ + data: [expect.objectContaining({ templateId: 'template-loss-runs' })], + }) + ) + expect(result.policyTasksCreated).toBe(1) + }) +}) diff --git a/ondeck/src/lib/sync/auto-generate.ts b/ondeck/src/lib/sync/auto-generate.ts index a63d5b2..c6f5bc8 100644 --- a/ondeck/src/lib/sync/auto-generate.ts +++ b/ondeck/src/lib/sync/auto-generate.ts @@ -1,5 +1,6 @@ import { prisma } from '@/lib/db' import { AUTOMATION_GO_LIVE_AT, type AutomationConfig } from './automation-config' +import { isRelevantDueDate } from './task-due-date' const DAY_MS = 24 * 60 * 60 * 1000 @@ -107,24 +108,27 @@ export async function runAutoGenerate(config: AutomationConfig): Promise { - 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 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, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) groupTasksCreated += created.count @@ -198,6 +202,7 @@ export async function runAutoGenerate(config: AutomationConfig): Promise isRelevantDueDate(task.dueDate)) if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) @@ -282,23 +287,26 @@ export async function runAutoGenerate(config: AutomationConfig): Promise { - 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: client.id, - templateId: template.id, - } - }) + const tasksToCreate = clientTemplates + .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: client.id, + templateId: template.id, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + if (tasksToCreate.length === 0) continue const created = await prisma.task.createMany({ data: tasksToCreate }) clientTasksCreated += created.count diff --git a/ondeck/src/lib/sync/task-due-date.ts b/ondeck/src/lib/sync/task-due-date.ts new file mode 100644 index 0000000..12c7a36 --- /dev/null +++ b/ondeck/src/lib/sync/task-due-date.ts @@ -0,0 +1,11 @@ +/** + * A template-generated task whose computed due date already fell in the past + * by the time generation ran (e.g. an already-superseded policy term, or a + * long lead-time offset generated late into the term) provides no value — + * it's born overdue with no chance anyone could have acted on it in time. + */ +export function isRelevantDueDate(dueDate: Date, now: Date = new Date()): boolean { + const startOfToday = new Date(now) + startOfToday.setHours(0, 0, 0, 0) + return dueDate >= startOfToday +}