386 lines
13 KiB
JavaScript
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())
|