seubert-claims/ondeck/scripts/historical-reset.mjs

386 lines
13 KiB
JavaScript

/**
* historical-reset.mjs
* Run with: node --env-file=.env scripts/historical-reset.mjs
*
* 1. Deletes all tasks + assignments
* 2. Deletes all policy groups (unlinks policies)
* 3. Runs recommendation engine per Shape client → creates groups
* 4. Generates tasks from templates (POLICY, RENEWAL_GROUP/BOTH, CLIENT)
* 5. Assigns tasks to existing claimsAdvocateId
*/
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'
import { readFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
// Load .env manually since --env-file may not work with all node versions
const __dirname = dirname(fileURLToPath(import.meta.url))
const envPath = resolve(__dirname, '../.env')
try {
const envFile = readFileSync(envPath, 'utf8')
for (const line of envFile.split('\n')) {
const match = line.match(/^([^#=]+)=(.*)$/)
if (match) process.env[match[1].trim()] = match[2].trim().replace(/^["']|["']$/g, '')
}
} catch {}
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaPg(pool)
const prisma = new PrismaClient({ adapter })
const WINDOW_DAYS = 30
const RULE = 'nearest-to-year-start'
// ── Recommendation engine (inline, mirrors renewal-group-recommendations.ts) ─
function addDays(date, days) {
const d = new Date(date)
d.setDate(d.getDate() + days)
return d
}
function dayOfYear(date) {
const start = new Date(date.getFullYear(), 0, 0)
return Math.floor((date - start) / 86400000)
}
function computeRenewalDate(policies, rule) {
const dates = policies.map((p) => p.expirationDate)
let picked
if (rule === 'earliest') {
picked = dates.reduce((a, b) => (a < b ? a : b))
} else if (rule === 'latest') {
picked = dates.reduce((a, b) => (a > b ? a : b))
} else {
picked = dates.reduce((a, b) => (dayOfYear(a) <= dayOfYear(b) ? a : b))
}
return addDays(picked, 1)
}
function recommendGroups(policies, { windowDays, rule }) {
const groupable = policies
.filter((p) => p.expirationDate)
.sort((a, b) => a.expirationDate - b.expirationDate)
const ungroupable = policies.filter((p) => !p.expirationDate)
if (groupable.length === 0) return { groups: [], ungroupable }
const clusters = []
let current = [groupable[0]]
let windowStart = groupable[0].expirationDate
for (let i = 1; i < groupable.length; i++) {
const diffDays = (groupable[i].expirationDate - windowStart) / 86400000
if (diffDays <= windowDays) {
current.push(groupable[i])
} else {
clusters.push(current)
current = [groupable[i]]
windowStart = groupable[i].expirationDate
}
}
clusters.push(current)
const groups = clusters.map((cluster) => ({
policies: cluster,
proposedRenewalDate: computeRenewalDate(cluster, rule),
}))
return { groups, ungroupable }
}
// ── Main ─────────────────────────────────────────────────────────────────────
async function main() {
console.log('=== Historical Reset ===\n')
// 1. Delete tasks + assignments
const delAssign = await prisma.taskAssignment.deleteMany({})
const delTasks = await prisma.task.deleteMany({})
console.log(`✓ Deleted ${delAssign.count} assignments, ${delTasks.count} tasks`)
// 2. Unlink policies from groups, then delete groups
await prisma.policy.updateMany({
where: { policyGroupId: { not: null } },
data: { policyGroupId: null },
})
const delGroups = await prisma.policyGroup.deleteMany({})
console.log(`✓ Deleted ${delGroups.count} policy groups`)
// 3. Reset setupCompletedAt on all clients
await prisma.client.updateMany({ data: { setupCompletedAt: null, renewalDate: null } })
console.log('✓ Reset setupCompletedAt on all clients\n')
// 4. Fetch Shape clients with active future policies
const today = new Date()
today.setHours(0, 0, 0, 0)
const clients = await prisma.client.findMany({
where: {
designation: { name: { in: ['Shape', 'Shape 2'] } },
},
select: {
id: true,
name: true,
claimsAdvocateId: true,
designationId: true,
designation2Id: true,
policies: {
where: {
expirationDate: { gte: today },
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
},
select: { id: true, expirationDate: true },
},
},
})
console.log(`Processing ${clients.length} Shape clients...\n`)
let groupsCreated = 0
let clientsSetup = 0
// 5. Re-run recommendations and save groups
for (const client of clients) {
if (client.policies.length === 0) continue
const inputs = client.policies.map((p) => ({
policyId: p.id,
expirationDate: new Date(p.expirationDate),
}))
const { groups } = recommendGroups(inputs, { windowDays: WINDOW_DAYS, rule: RULE })
if (groups.length === 0) continue
await prisma.$transaction(async (tx) => {
for (let i = 0; i < groups.length; i++) {
const g = groups[i]
const created = await tx.policyGroup.create({
data: {
clientId: client.id,
name: `Group ${i + 1}`,
renewalDate: g.proposedRenewalDate,
},
})
groupsCreated++
if (g.policies.length > 0) {
await tx.policy.updateMany({
where: { id: { in: g.policies.map((p) => p.policyId) }, clientId: client.id },
data: { policyGroupId: created.id },
})
}
}
await tx.client.update({
where: { id: client.id },
data: {
renewalDate: groups[0].proposedRenewalDate,
setupCompletedAt: new Date(),
},
})
})
clientsSetup++
}
console.log(`✓ Created ${groupsCreated} groups across ${clientsSetup} clients\n`)
// 6. Generate tasks ─────────────────────────────────────────────────────────
let groupTasksCreated = 0
let policyTasksCreated = 0
let clientTasksCreated = 0
let errors = 0
// 6a. Group-level tasks
const allGroups = await prisma.policyGroup.findMany({
where: { policies: { some: {} } },
include: {
client: { select: { designationId: true, designation2Id: true, claimsAdvocateId: true } },
},
})
for (const group of allGroups) {
try {
const desIds = [group.client.designationId, group.client.designation2Id].filter(Boolean)
const templates = await prisma.taskTemplate.findMany({
where: {
isActive: true,
level: { in: ['BOTH', 'RENEWAL_GROUP'] },
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
},
})
if (templates.length === 0) continue
const renewalDate = new Date(group.renewalDate)
const tasksToCreate = templates.map((t) => {
const dueDate = new Date(renewalDate)
dueDate.setDate(dueDate.getDate() + t.daysOffset)
return {
title: t.name, description: t.description, department: t.department,
timing: t.timing, daysOffset: t.daysOffset, dueDate,
status: 'NOT_STARTED', priority: t.defaultPriority,
clientId: group.clientId, policyGroupId: group.id, templateId: t.id,
}
})
const created = await prisma.task.createMany({ data: tasksToCreate, skipDuplicates: true })
groupTasksCreated += created.count
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 },
})
await prisma.taskAssignment.createMany({
data: newTasks.map((t) => ({ taskId: t.id, userId: group.client.claimsAdvocateId })),
skipDuplicates: true,
})
}
} catch (e) {
console.error(`Group ${group.id}: ${e.message}`)
errors++
}
}
// 6b. Policy-level tasks (ungrouped policies only)
const ungroupedPolicies = await prisma.policy.findMany({
where: {
policyGroupId: null,
expirationDate: { gte: today },
status: { notIn: ['Cancelled', 'Expired', 'Non-Renewed', 'Rewritten', 'Not taken'] },
client: { designation: { name: { in: ['Shape', 'Shape 2'] } } },
},
select: {
id: true, expirationDate: true, clientId: true,
client: { select: { designationId: true, designation2Id: true, claimsAdvocateId: true } },
},
})
for (const policy of ungroupedPolicies) {
try {
const desIds = [policy.client.designationId, policy.client.designation2Id].filter(Boolean)
const templates = await prisma.taskTemplate.findMany({
where: {
isActive: true,
level: { in: ['BOTH', 'POLICY'] },
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
},
})
if (templates.length === 0) continue
const anchorDate = new Date(policy.expirationDate)
anchorDate.setDate(anchorDate.getDate() + 1)
const tasksToCreate = templates.map((t) => {
const dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + t.daysOffset)
return {
title: t.name, description: t.description, department: t.department,
timing: t.timing, daysOffset: t.daysOffset, dueDate,
status: 'NOT_STARTED', priority: t.defaultPriority,
clientId: policy.clientId, policyId: policy.id, templateId: t.id,
}
})
const created = await prisma.task.createMany({ data: tasksToCreate, skipDuplicates: true })
policyTasksCreated += created.count
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 },
})
await prisma.taskAssignment.createMany({
data: newTasks.map((t) => ({ taskId: t.id, userId: policy.client.claimsAdvocateId })),
skipDuplicates: true,
})
}
} catch (e) {
console.error(`Policy ${policy.id}: ${e.message}`)
errors++
}
}
// 6c. CLIENT-level tasks (once per client)
const allClientIds = [...new Set(allGroups.map((g) => g.clientId))]
for (const clientId of allClientIds) {
try {
const clientRecord = await prisma.client.findUnique({
where: { id: clientId },
select: {
designationId: true, designation2Id: true, claimsAdvocateId: true,
policyGroups: { select: { renewalDate: true } },
policies: { where: { policyGroupId: null }, select: { expirationDate: true } },
},
})
if (!clientRecord) continue
const desIds = [clientRecord.designationId, clientRecord.designation2Id].filter(Boolean)
const clientTemplates = await prisma.taskTemplate.findMany({
where: {
isActive: true, level: 'CLIENT',
OR: [{ designationId: null }, ...(desIds.length ? [{ designationId: { in: desIds } }] : [])],
},
})
if (clientTemplates.length === 0) 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 dueDate = new Date(anchorDate)
dueDate.setDate(dueDate.getDate() + template.daysOffset)
const created = 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,
},
select: { id: true },
})
clientTasksCreated++
if (clientRecord.claimsAdvocateId) {
await prisma.taskAssignment.create({
data: { taskId: created.id, userId: clientRecord.claimsAdvocateId },
})
}
}
} catch (e) {
console.error(`Client ${clientId} (CLIENT tasks): ${e.message}`)
errors++
}
}
console.log(`✓ Tasks created:`)
console.log(` Group-level: ${groupTasksCreated}`)
console.log(` Policy-level: ${policyTasksCreated}`)
console.log(` Client-level: ${clientTasksCreated}`)
console.log(` Total: ${groupTasksCreated + policyTasksCreated + clientTasksCreated}`)
if (errors) console.log(` Errors: ${errors}`)
console.log('\n=== Done ===')
}
main()
.catch((e) => { console.error(e); process.exit(1) })
.finally(() => prisma.$disconnect())