From 06842f9031f5637e37bcd069f1e7909ac6a78934 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 18 Jul 2026 10:24:07 +0000 Subject: [PATCH] fix(tasks): skip audit log on zero-template runs, add POLICY/CLIENT/multi-section test coverage - runGenerateAndAssign now returns early (no audit log write, no client-name lookup) when no active templates match the resolved designation, mirroring the existing early return for zero matched clients. - Add tests for: zero-template real run writes no audit log; POLICY-level template against an ungrouped policy; CLIENT-level template created via prisma.task.create; and a single client accumulating estimatedTasks from both a group-level and a policy-level template in the same run. --- .../generate-and-assign-core.test.ts | 115 +++++++ .../src/lib/tasks/generate-and-assign-core.ts | 302 +++++++++--------- 2 files changed, 271 insertions(+), 146 deletions(-) diff --git a/ondeck/src/lib/tasks/__tests__/generate-and-assign-core.test.ts b/ondeck/src/lib/tasks/__tests__/generate-and-assign-core.test.ts index aafcb2b..639577a 100644 --- a/ondeck/src/lib/tasks/__tests__/generate-and-assign-core.test.ts +++ b/ondeck/src/lib/tasks/__tests__/generate-and-assign-core.test.ts @@ -138,6 +138,121 @@ describe('runGenerateAndAssign — advocate-designation mode', () => { status: 404, }) }) + + it('does not write an audit log and returns zero counts when no templates match', async () => { + mockUserFindUnique.mockResolvedValue({ id: 'adv-1', displayName: 'Jane Smith', isActive: true }) + mockClientsForAdvocateDesignation() + mockTaskTemplateFindMany.mockResolvedValue([]) + + const result = await runGenerateAndAssign(target, { dryRun: false, actorUserId: 'user-1' }) + + expect(mockAuditLogCreate).not.toHaveBeenCalled() + expect(result.totalEstimatedTasks).toBe(0) + expect(result.tasksCreated).toBe(0) + }) + + it('creates and counts a POLICY-level task for an ungrouped policy', async () => { + mockUserFindUnique.mockResolvedValue({ id: 'adv-1', displayName: 'Jane Smith', isActive: true }) + mockClientsForAdvocateDesignation() + mockTaskTemplateFindMany.mockResolvedValue([ + { + id: 'template-policy', + name: 'Review policy renewal', + description: null, + department: 'Personal Lines', + timing: 'PRE_RENEWAL', + daysOffset: -1, + defaultPriority: 'NORMAL', + level: 'POLICY', + }, + ]) + mockPolicyFindMany.mockResolvedValue([ + { id: 'policy-1', clientId: 'client-1', policyType: 'AUTO', expirationDate: new Date('2026-08-01') }, + ]) + mockTaskCreateMany.mockResolvedValue({ count: 1 }) + mockTaskFindMany.mockResolvedValue([{ id: 'task-policy-1' }]) + mockTaskAssignmentCreateMany.mockResolvedValue({ count: 1 }) + + const dryRunResult = await runGenerateAndAssign(target, { dryRun: true, actorUserId: 'user-1' }) + const realResult = await runGenerateAndAssign(target, { dryRun: false, actorUserId: 'user-1' }) + + expect(dryRunResult.totalEstimatedTasks).toBe(1) + expect(dryRunResult.clients).toEqual([{ id: 'client-1', name: 'Client One', estimatedTasks: 1 }]) + expect(mockTaskCreateMany).toHaveBeenCalledTimes(1) + expect(realResult.tasksCreated).toBe(1) + }) + + it('creates and counts a CLIENT-level task via prisma.task.create', async () => { + mockUserFindUnique.mockResolvedValue({ id: 'adv-1', displayName: 'Jane Smith', isActive: true }) + mockClientsForAdvocateDesignation() + mockTaskTemplateFindMany.mockResolvedValue([ + { + id: 'template-client', + name: 'Send client renewal letter', + description: null, + department: 'Commercial Lines', + timing: 'PRE_RENEWAL', + daysOffset: -1, + defaultPriority: 'NORMAL', + level: 'CLIENT', + }, + ]) + mockClientFindUnique.mockResolvedValue({ + policyGroups: [{ renewalDate: new Date('2026-08-01') }], + policies: [], + }) + mockTaskFindFirst.mockResolvedValue(null) + mockTaskCreate.mockResolvedValue({ id: 'task-client-1' }) + mockTaskAssignmentCreate.mockResolvedValue({}) + + const dryRunResult = await runGenerateAndAssign(target, { dryRun: true, actorUserId: 'user-1' }) + const realResult = await runGenerateAndAssign(target, { dryRun: false, actorUserId: 'user-1' }) + + expect(dryRunResult.totalEstimatedTasks).toBe(1) + expect(dryRunResult.clients).toEqual([{ id: 'client-1', name: 'Client One', estimatedTasks: 1 }]) + expect(mockTaskCreate).toHaveBeenCalledTimes(1) + expect(mockTaskCreateMany).not.toHaveBeenCalled() + expect(realResult.tasksCreated).toBe(1) + expect(realResult.tasksAssigned).toBe(1) + }) + + it('sums estimatedTasks for a client that gets tasks from both group- and policy-level templates', async () => { + mockUserFindUnique.mockResolvedValue({ id: 'adv-1', displayName: 'Jane Smith', isActive: true }) + mockClientsForAdvocateDesignation() + mockTaskTemplateFindMany.mockResolvedValue([ + { + id: 'template-group', + name: 'Prepare loss summary', + description: null, + department: 'Commercial Lines', + timing: 'PRE_RENEWAL', + daysOffset: -1, + defaultPriority: 'NORMAL', + level: 'RENEWAL_GROUP', + }, + { + id: 'template-policy', + name: 'Review policy renewal', + description: null, + department: 'Personal Lines', + timing: 'PRE_RENEWAL', + daysOffset: -1, + defaultPriority: 'NORMAL', + level: 'POLICY', + }, + ]) + mockPolicyGroupFindMany.mockResolvedValue([ + { id: 'group-1', clientId: 'client-1', renewalDate: new Date('2026-08-01') }, + ]) + mockPolicyFindMany.mockResolvedValue([ + { id: 'policy-1', clientId: 'client-1', policyType: 'AUTO', expirationDate: new Date('2026-08-01') }, + ]) + + const result = await runGenerateAndAssign(target, { dryRun: true, actorUserId: 'user-1' }) + + expect(result.clients).toEqual([{ id: 'client-1', name: 'Client One', estimatedTasks: 2 }]) + expect(result.totalEstimatedTasks).toBe(2) + }) }) describe('parseGenerateAndAssignTarget', () => { diff --git a/ondeck/src/lib/tasks/generate-and-assign-core.ts b/ondeck/src/lib/tasks/generate-and-assign-core.ts index f8a1142..8c58b32 100644 --- a/ondeck/src/lib/tasks/generate-and-assign-core.ts +++ b/ondeck/src/lib/tasks/generate-and-assign-core.ts @@ -140,166 +140,176 @@ export async function runGenerateAndAssign( orderBy: [{ department: 'asc' }, { daysOffset: 'asc' }], }) - if (allTemplates.length > 0) { - const groupTemplates = allTemplates.filter((t) => t.level === 'BOTH' || t.level === 'RENEWAL_GROUP') - const policyTemplates = allTemplates.filter((t) => t.level === 'BOTH' || t.level === 'POLICY') - const clientTemplates = allTemplates.filter((t) => t.level === 'CLIENT') + if (allTemplates.length === 0) { + return { + dryRun: options.dryRun, + clientsFound: resolved.clientIds.length, + totalEstimatedTasks: 0, + advocateName: resolved.advocateName, + clients: [], + tasksCreated: 0, + tasksAssigned: 0, + } + } - const groups = await prisma.policyGroup.findMany({ - where: { clientId: { in: resolved.clientIds }, tasks: { none: { templateId: { not: null } } } }, - }) + const groupTemplates = allTemplates.filter((t) => t.level === 'BOTH' || t.level === 'RENEWAL_GROUP') + const policyTemplates = allTemplates.filter((t) => t.level === 'BOTH' || t.level === 'POLICY') + const clientTemplates = allTemplates.filter((t) => t.level === 'CLIENT') - 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 { + const groups = await prisma.policyGroup.findMany({ + where: { clientId: { in: resolved.clientIds }, tasks: { none: { templateId: { not: null } } } }, + }) + + 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: options.actorUserId, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + + if (tasksToCreate.length === 0) continue + estimatedByClient.set(group.clientId, (estimatedByClient.get(group.clientId) ?? 0) + tasksToCreate.length) + if (options.dryRun) continue + + const created = await prisma.task.createMany({ data: tasksToCreate }) + tasksCreated += created.count + + if (created.count > 0) { + const newTasks = await prisma.task.findMany({ + where: { policyGroupId: group.id, templateId: { in: groupTemplates.map((t) => t.id) } }, + select: { id: true }, + }) + if (newTasks.length > 0) { + const assigned = await prisma.taskAssignment.createMany({ + data: newTasks.map((t) => ({ taskId: t.id, userId: resolved.advocateId })), + skipDuplicates: true, + }) + tasksAssigned += assigned.count + } + } + } + + const policies = await prisma.policy.findMany({ + where: { clientId: { in: resolved.clientIds }, policyGroupId: null, tasks: { none: { templateId: { not: null } } } }, + }) + + for (const policy of policies) { + const anchorDate = new Date(policy.expirationDate) + anchorDate.setDate(anchorDate.getDate() + 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: options.actorUserId, + } + }) + .filter((task) => isRelevantDueDate(task.dueDate)) + + if (tasksToCreate.length === 0) continue + estimatedByClient.set(policy.clientId, (estimatedByClient.get(policy.clientId) ?? 0) + tasksToCreate.length) + if (options.dryRun) continue + + const created = await prisma.task.createMany({ data: tasksToCreate }) + tasksCreated += created.count + + if (created.count > 0) { + const newTasks = await prisma.task.findMany({ + where: { policyId: policy.id, templateId: { in: policyTemplates.map((t) => t.id) } }, + select: { id: true }, + }) + if (newTasks.length > 0) { + const assigned = await prisma.taskAssignment.createMany({ + data: newTasks.map((t) => ({ taskId: t.id, userId: resolved.advocateId })), + skipDuplicates: true, + }) + tasksAssigned += assigned.count + } + } + } + + if (clientTemplates.length > 0) { + for (const clientId of resolved.clientIds) { + const clientRecord = await prisma.client.findUnique({ + where: { id: clientId }, + select: { + policyGroups: { select: { renewalDate: true } }, + policies: { where: { policyGroupId: null }, select: { expirationDate: true } }, + }, + }) + if (!clientRecord) continue + + const groupDates = clientRecord.policyGroups.map((g) => new Date(g.renewalDate).getTime()) + const policyDates = clientRecord.policies.map((p) => { + const d = new Date(p.expirationDate) + d.setDate(d.getDate() + 1) + return d.getTime() + }) + const allDates = [...groupDates, ...policyDates] + if (allDates.length === 0) continue + const anchorDate = new Date(Math.min(...allDates)) + + for (const template of clientTemplates) { + const existing = await prisma.task.findFirst({ + where: { clientId, templateId: template.id, policyId: null, policyGroupId: null }, + select: { id: true }, + }) + if (existing) continue + + const dueDate = new Date(anchorDate) + dueDate.setDate(dueDate.getDate() + template.daysOffset) + if (!isRelevantDueDate(dueDate)) continue + + estimatedByClient.set(clientId, (estimatedByClient.get(clientId) ?? 0) + 1) + if (options.dryRun) continue + + const createdTask = await prisma.task.create({ + data: { title: template.name, description: template.description, department: template.department, timing: template.timing, daysOffset: template.daysOffset, dueDate, - status: 'NOT_STARTED' as const, + status: 'NOT_STARTED', priority: template.defaultPriority, - clientId: group.clientId, - policyGroupId: group.id, + clientId, templateId: template.id, createdBy: options.actorUserId, - } - }) - .filter((task) => isRelevantDueDate(task.dueDate)) - - if (tasksToCreate.length === 0) continue - estimatedByClient.set(group.clientId, (estimatedByClient.get(group.clientId) ?? 0) + tasksToCreate.length) - if (options.dryRun) continue - - const created = await prisma.task.createMany({ data: tasksToCreate }) - tasksCreated += created.count - - if (created.count > 0) { - const newTasks = await prisma.task.findMany({ - where: { policyGroupId: group.id, templateId: { in: groupTemplates.map((t) => t.id) } }, - select: { id: true }, - }) - if (newTasks.length > 0) { - const assigned = await prisma.taskAssignment.createMany({ - data: newTasks.map((t) => ({ taskId: t.id, userId: resolved.advocateId })), - skipDuplicates: true, - }) - tasksAssigned += assigned.count - } - } - } - - const policies = await prisma.policy.findMany({ - where: { clientId: { in: resolved.clientIds }, policyGroupId: null, tasks: { none: { templateId: { not: null } } } }, - }) - - for (const policy of policies) { - const anchorDate = new Date(policy.expirationDate) - anchorDate.setDate(anchorDate.getDate() + 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: options.actorUserId, - } - }) - .filter((task) => isRelevantDueDate(task.dueDate)) - - if (tasksToCreate.length === 0) continue - estimatedByClient.set(policy.clientId, (estimatedByClient.get(policy.clientId) ?? 0) + tasksToCreate.length) - if (options.dryRun) continue - - const created = await prisma.task.createMany({ data: tasksToCreate }) - tasksCreated += created.count - - if (created.count > 0) { - const newTasks = await prisma.task.findMany({ - where: { policyId: policy.id, templateId: { in: policyTemplates.map((t) => t.id) } }, - select: { id: true }, - }) - if (newTasks.length > 0) { - const assigned = await prisma.taskAssignment.createMany({ - data: newTasks.map((t) => ({ taskId: t.id, userId: resolved.advocateId })), - skipDuplicates: true, - }) - tasksAssigned += assigned.count - } - } - } - - if (clientTemplates.length > 0) { - for (const clientId of resolved.clientIds) { - const clientRecord = await prisma.client.findUnique({ - where: { id: clientId }, - select: { - policyGroups: { select: { renewalDate: true } }, - policies: { where: { policyGroupId: null }, select: { expirationDate: true } }, }, + select: { id: true }, }) - if (!clientRecord) continue + tasksCreated++ - const groupDates = clientRecord.policyGroups.map((g) => new Date(g.renewalDate).getTime()) - const policyDates = clientRecord.policies.map((p) => { - const d = new Date(p.expirationDate) - d.setDate(d.getDate() + 1) - return d.getTime() - }) - const allDates = [...groupDates, ...policyDates] - if (allDates.length === 0) continue - const anchorDate = new Date(Math.min(...allDates)) - - for (const template of clientTemplates) { - const existing = await prisma.task.findFirst({ - where: { clientId, templateId: template.id, policyId: null, policyGroupId: null }, - select: { id: true }, - }) - if (existing) continue - - const dueDate = new Date(anchorDate) - dueDate.setDate(dueDate.getDate() + template.daysOffset) - if (!isRelevantDueDate(dueDate)) continue - - estimatedByClient.set(clientId, (estimatedByClient.get(clientId) ?? 0) + 1) - if (options.dryRun) continue - - const createdTask = await prisma.task.create({ - data: { - title: template.name, - description: template.description, - department: template.department, - timing: template.timing, - daysOffset: template.daysOffset, - dueDate, - status: 'NOT_STARTED', - priority: template.defaultPriority, - clientId, - templateId: template.id, - createdBy: options.actorUserId, - }, - select: { id: true }, - }) - tasksCreated++ - - await prisma.taskAssignment.create({ data: { taskId: createdTask.id, userId: resolved.advocateId } }) - tasksAssigned++ - } + await prisma.taskAssignment.create({ data: { taskId: createdTask.id, userId: resolved.advocateId } }) + tasksAssigned++ } } }