- Add TaskAudit/TaskAuditStatus Prisma models linked to Client/Task/User.
- Add ImageRightClient (auth, findFilesByFileNumber, findDocuments,
getSortedFolders) per Vertafore REST v1 API.
- Add audit engine that resolves per-item target dates/windows from the
SHAPE_IR_Filing_Audit_Spec.md checklist, navigates real ImageRight folder
structure, and links results to existing Tasks where found.
- Add GET/POST /api/clients/[id]/task-audit (SHAPE/SHAPE2-gated) and a
Document Audit tab in the client detail page.
- Add Client.amsCustomerNumber (AFW_Customer.CustNo) — ImageRight's
FileNumberPart1 expects this AMS360 account number, not the amsCustomerId
GUID. Backfilled via one-off /api/admin/backfill-ams-customer-number.
- Fix folder/document traversal against verified live ImageRight data:
real folders are nested under a per-year "Policy Term" folder (not flat
SUBMISSION/PRERENEWAL as in the source spec doc), and document listing
requires POST /api/documents/find with {FileId, ParentId} — the
previously assumed /api/containers/{id} endpoint 404s on real ids.
- Add jest coverage for client auth/find calls and folder/doc-type matching.
Known open items (need process-owner input, not resolved here):
- Real IR document types (Correspondences, Pre-Renewal Information, Loss
Run) don't map 1:1 to the spec's generic labels (EMAIL, EXCEL DOC, PDF).
- Spec's second folder_path segment (e.g. PRERENEWAL) has no matching real
subfolder for at least one tested client; currently ignored for folder
navigation and only surfaced in the audit result label.
- Whether an audit item should be skipped when no open Task exists for it,
rather than always running and reporting INCOMPLETE.
556 lines
19 KiB
Text
556 lines
19 KiB
Text
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ============================================
|
|
// User Management & Authentication
|
|
// ============================================
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
entraOid String? @unique @map("entra_oid")
|
|
email String @unique
|
|
displayName String? @map("display_name")
|
|
department String?
|
|
jobTitle String? @map("job_title")
|
|
photoUrl String? @map("photo_url")
|
|
office String?
|
|
isActive Boolean @default(true) @map("is_active")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
userRoles UserRole[]
|
|
createdTasks Task[] @relation("TaskCreatedBy")
|
|
completedTasks Task[] @relation("TaskCompletedBy")
|
|
taskAssignments TaskAssignment[]
|
|
taskNotes TaskNote[] @relation("TaskNoteAuthor")
|
|
createdTemplates TaskTemplate[]
|
|
syncLogs SyncLog[]
|
|
auditLogs AuditLog[]
|
|
notifications Notification[]
|
|
notificationPrefs NotificationPreference[]
|
|
createdPolicyGroups PolicyGroup[]
|
|
advocateClients Client[] @relation("ClientAdvocate")
|
|
clientMemberships ClientMember[]
|
|
shapeImportRuns ShapeImportRun[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Role {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
permissions Json @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
userRoles UserRole[]
|
|
entraGroupMappings EntraGroupRoleMapping[]
|
|
|
|
@@map("roles")
|
|
}
|
|
|
|
model UserRole {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
roleId String @map("role_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, roleId])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model EntraGroupRoleMapping {
|
|
id String @id @default(cuid())
|
|
entraGroupId String @map("entra_group_id")
|
|
roleId String @map("role_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([entraGroupId, roleId])
|
|
@@map("entra_group_role_mappings")
|
|
}
|
|
|
|
// ============================================
|
|
// Client & Policy Management
|
|
// ============================================
|
|
|
|
model Designation {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
color String
|
|
rules String?
|
|
displayOrder Int @map("display_order")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
afwAnotId String? @unique @map("afw_anot_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
clientsDesignation1 Client[] @relation("ClientDesignation1")
|
|
clientsDesignation2 Client[] @relation("ClientDesignation2")
|
|
taskTemplates TaskTemplate[]
|
|
|
|
@@map("designations")
|
|
}
|
|
|
|
model Client {
|
|
id String @id @default(cuid())
|
|
amsCustomerId String @unique @map("ams_customer_id")
|
|
/// AFW's short numeric customer/account number (AFW_Customer.CustNo) — distinct from
|
|
/// amsCustomerId (AFW's internal GUID). This is what ImageRight's FileNumberPart1 expects.
|
|
amsCustomerNumber Int? @map("ams_customer_number")
|
|
name String
|
|
addressLine1 String? @map("address_line1")
|
|
addressLine2 String? @map("address_line2")
|
|
city String?
|
|
state String?
|
|
zipCode String? @map("zip_code")
|
|
phone String?
|
|
email String?
|
|
producerCode String? @map("producer_code")
|
|
amsCreatedAt DateTime? @map("ams_created_at")
|
|
amsModifiedAt DateTime? @map("ams_modified_at")
|
|
designationId String? @map("designation_id")
|
|
designation2Id String? @map("designation2_id")
|
|
claimsAdvocateId String? @map("claims_advocate_id")
|
|
parentClientId String? @map("parent_client_id")
|
|
notes String? @db.Text
|
|
customFields Json @default("{}") @map("custom_fields")
|
|
renewalDate DateTime? @map("renewal_date")
|
|
setupCompletedAt DateTime? @map("setup_completed_at")
|
|
lastSyncedAt DateTime? @map("last_synced_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
designation Designation? @relation("ClientDesignation1", fields: [designationId], references: [id])
|
|
designation2 Designation? @relation("ClientDesignation2", fields: [designation2Id], references: [id])
|
|
claimsAdvocate User? @relation("ClientAdvocate", fields: [claimsAdvocateId], references: [id])
|
|
parentClient Client? @relation("ClientParent", fields: [parentClientId], references: [id])
|
|
subsidiaries Client[] @relation("ClientParent")
|
|
policies Policy[]
|
|
tasks Task[]
|
|
policyGroups PolicyGroup[]
|
|
members ClientMember[]
|
|
contacts ClientContact[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@index([name])
|
|
@@index([designationId])
|
|
@@index([designation2Id])
|
|
@@index([claimsAdvocateId])
|
|
@@index([parentClientId])
|
|
@@map("clients")
|
|
}
|
|
|
|
model ClientContact {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
label String
|
|
name String
|
|
title String?
|
|
phone String?
|
|
mobilePhone String? @map("mobile_phone")
|
|
email String?
|
|
notes String? @db.Text
|
|
source String @default("manual")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([clientId, name, label, source], name: "client_contact_dedup")
|
|
@@index([clientId])
|
|
@@map("client_contacts")
|
|
}
|
|
|
|
model ClientMember {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
userId String @map("user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([clientId, userId])
|
|
@@index([clientId])
|
|
@@index([userId])
|
|
@@map("client_members")
|
|
}
|
|
|
|
model PolicyGroup {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
name String
|
|
renewalDate DateTime @map("renewal_date")
|
|
notes String? @db.Text
|
|
createdBy String? @map("created_by")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
creator User? @relation(fields: [createdBy], references: [id])
|
|
policies Policy[]
|
|
tasks Task[]
|
|
|
|
@@index([clientId])
|
|
@@index([renewalDate])
|
|
@@map("policy_groups")
|
|
}
|
|
|
|
model Policy {
|
|
id String @id @default(cuid())
|
|
amsPolicyId String @unique @map("ams_policy_id")
|
|
clientId String @map("client_id")
|
|
policyGroupId String? @map("policy_group_id")
|
|
policyNumber String? @map("policy_number")
|
|
policyType String? @map("policy_type")
|
|
effectiveDate DateTime? @map("effective_date")
|
|
expirationDate DateTime @map("expiration_date")
|
|
carrierName String? @map("carrier_name")
|
|
writingCompanyName String? @map("writing_company_name")
|
|
premiumAmount Decimal? @map("premium_amount") @db.Decimal(12, 2)
|
|
status String?
|
|
billMethod String? @map("bill_method")
|
|
businessType String? @map("business_type")
|
|
department String?
|
|
executiveName String? @map("executive_name")
|
|
csrName String? @map("csr_name")
|
|
additionalRep1 String? @map("additional_rep1")
|
|
additionalRep2 String? @map("additional_rep2")
|
|
additionalExec1 String? @map("additional_exec1")
|
|
additionalExec2 String? @map("additional_exec2")
|
|
amsCreatedAt DateTime? @map("ams_created_at")
|
|
amsModifiedAt DateTime? @map("ams_modified_at")
|
|
lastSyncedAt DateTime? @map("last_synced_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
policyGroup PolicyGroup? @relation(fields: [policyGroupId], references: [id])
|
|
tasks Task[]
|
|
|
|
@@index([clientId])
|
|
@@index([policyGroupId])
|
|
@@index([expirationDate])
|
|
@@index([department])
|
|
@@map("policies")
|
|
}
|
|
|
|
// ============================================
|
|
// Task Management
|
|
// ============================================
|
|
|
|
enum TaskStatus {
|
|
NOT_STARTED
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
BLOCKED
|
|
NA
|
|
CANCELLED
|
|
}
|
|
|
|
enum TaskPriority {
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
URGENT
|
|
}
|
|
|
|
enum TaskTiming {
|
|
PRE_RENEWAL
|
|
POST_RENEWAL
|
|
}
|
|
|
|
enum TaskLevel {
|
|
POLICY
|
|
RENEWAL_GROUP
|
|
BOTH
|
|
CLIENT
|
|
}
|
|
|
|
enum DepartmentType {
|
|
PERSONAL_LINES
|
|
COMMERCIAL_LINES
|
|
CLAIMS
|
|
BENEFITS
|
|
OTHER
|
|
}
|
|
|
|
enum TaskAuditStatus {
|
|
COMPLETE
|
|
INCOMPLETE
|
|
NOT_APPLICABLE
|
|
}
|
|
|
|
model TaskTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String? @db.Text
|
|
department DepartmentType
|
|
timing TaskTiming
|
|
daysOffset Int @map("days_offset")
|
|
defaultPriority TaskPriority @map("default_priority")
|
|
taskGroup String? @map("task_group")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
displayOrder Int? @map("display_order")
|
|
level TaskLevel @default(BOTH)
|
|
designationId String? @map("designation_id")
|
|
policyTypeFilter String? @map("policy_type_filter")
|
|
createdBy String? @map("created_by")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
designation Designation? @relation(fields: [designationId], references: [id])
|
|
creator User? @relation(fields: [createdBy], references: [id])
|
|
tasks Task[]
|
|
|
|
@@index([designationId])
|
|
@@map("task_templates")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
department DepartmentType
|
|
timing TaskTiming
|
|
daysOffset Int @map("days_offset")
|
|
dueDate DateTime @map("due_date")
|
|
status TaskStatus @default(NOT_STARTED)
|
|
priority TaskPriority
|
|
clientId String @map("client_id")
|
|
policyId String? @map("policy_id")
|
|
policyGroupId String? @map("policy_group_id")
|
|
templateId String? @map("template_id")
|
|
createdBy String? @map("created_by")
|
|
completedAt DateTime? @map("completed_at")
|
|
completedBy String? @map("completed_by")
|
|
notes String? @db.Text
|
|
isAdHoc Boolean @default(false) @map("is_ad_hoc")
|
|
naReason String? @map("na_reason") @db.Text
|
|
cancelledReason String? @map("cancelled_reason") @db.Text
|
|
taskGroup String? @map("task_group")
|
|
imageRightFiled Boolean? @map("image_right_filed")
|
|
reminderDate DateTime? @map("reminder_date")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
policy Policy? @relation(fields: [policyId], references: [id])
|
|
policyGroup PolicyGroup? @relation(fields: [policyGroupId], references: [id])
|
|
template TaskTemplate? @relation(fields: [templateId], references: [id])
|
|
creator User? @relation("TaskCreatedBy", fields: [createdBy], references: [id])
|
|
completer User? @relation("TaskCompletedBy", fields: [completedBy], references: [id])
|
|
assignments TaskAssignment[]
|
|
taskNotes TaskNote[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@index([clientId])
|
|
@@index([policyId])
|
|
@@index([policyGroupId])
|
|
@@index([status])
|
|
@@index([dueDate])
|
|
@@index([department])
|
|
@@map("tasks")
|
|
}
|
|
|
|
model TaskAssignment {
|
|
id String @id @default(cuid())
|
|
taskId String @map("task_id")
|
|
userId String @map("user_id")
|
|
assignedAt DateTime @default(now()) @map("assigned_at")
|
|
|
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([taskId, userId])
|
|
@@map("task_assignments")
|
|
}
|
|
|
|
/// Result of checking one SHAPE_IR_Filing_Audit_Spec.md checklist item against
|
|
/// ImageRight for a client. Each audit run inserts new rows (history preserved);
|
|
/// the latest row per (clientId, specItemKey) is the current status. Optionally
|
|
/// linked to an existing generated Task, but not required — a spec item with no
|
|
/// matching Task is still auditable and surfaces as a gap.
|
|
model TaskAudit {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
taskId String? @map("task_id")
|
|
specItemKey String @map("spec_item_key")
|
|
targetDate DateTime @map("target_date")
|
|
status TaskAuditStatus
|
|
matchedDocId String? @map("matched_doc_id")
|
|
matchedDocName String? @map("matched_doc_name")
|
|
matchedDocDate DateTime? @map("matched_doc_date")
|
|
folderChecked String? @map("folder_checked")
|
|
errorMessage String? @map("error_message") @db.Text
|
|
runBy String? @map("run_by")
|
|
runAt DateTime @default(now()) @map("run_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
|
runByUser User? @relation(fields: [runBy], references: [id])
|
|
|
|
@@index([clientId])
|
|
@@index([specItemKey])
|
|
@@index([clientId, specItemKey, runAt])
|
|
@@map("task_audits")
|
|
}
|
|
|
|
model TaskNote {
|
|
id String @id @default(cuid())
|
|
taskId String @map("task_id")
|
|
userId String @map("user_id")
|
|
content String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
user User @relation("TaskNoteAuthor", fields: [userId], references: [id])
|
|
|
|
@@index([taskId])
|
|
@@map("task_notes")
|
|
}
|
|
|
|
// ============================================
|
|
// Sync & System Management
|
|
// ============================================
|
|
|
|
model SyncLog {
|
|
id String @id @default(cuid())
|
|
syncType String @map("sync_type")
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
status String
|
|
rowsProcessed Int @default(0) @map("rows_processed")
|
|
rowsInserted Int @default(0) @map("rows_inserted")
|
|
rowsUpdated Int @default(0) @map("rows_updated")
|
|
errorMessage String? @map("error_message") @db.Text
|
|
triggeredBy String? @map("triggered_by")
|
|
|
|
user User? @relation(fields: [triggeredBy], references: [id])
|
|
|
|
@@index([syncType])
|
|
@@index([startedAt])
|
|
@@map("sync_logs")
|
|
}
|
|
|
|
model SyncConfig {
|
|
id String @id @default(cuid())
|
|
key String @unique
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("sync_config")
|
|
}
|
|
|
|
model AppSetting {
|
|
key String @id
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("app_settings")
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
userId String? @map("user_id")
|
|
action String
|
|
entityType String @map("entity_type")
|
|
entityId String? @map("entity_id")
|
|
oldValues Json? @map("old_values")
|
|
newValues Json? @map("new_values")
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent") @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User? @relation(fields: [userId], references: [id])
|
|
|
|
@@index([userId])
|
|
@@index([entityType])
|
|
@@index([createdAt])
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
// ============================================
|
|
// Notifications
|
|
// ============================================
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
type String
|
|
title String
|
|
message String @db.Text
|
|
relatedEntityType String? @map("related_entity_type")
|
|
relatedEntityId String? @map("related_entity_id")
|
|
isRead Boolean @default(false) @map("is_read")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, isRead])
|
|
@@index([createdAt])
|
|
@@map("notifications")
|
|
}
|
|
|
|
model ShapeImportRun {
|
|
id String @id @default(cuid())
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
status String // pending | running | completed | failed
|
|
dryRun Boolean @default(true) @map("dry_run")
|
|
driveId String @map("drive_id")
|
|
folderPath String @default("Claims/SHAPE Accounts") @map("folder_path")
|
|
triggeredBy String? @map("triggered_by")
|
|
stats Json?
|
|
|
|
user User? @relation(fields: [triggeredBy], references: [id])
|
|
logLines ShapeImportLog[]
|
|
|
|
@@index([startedAt])
|
|
@@map("shape_import_runs")
|
|
}
|
|
|
|
model ShapeImportLog {
|
|
id String @id @default(cuid())
|
|
runId String @map("run_id")
|
|
seq Int
|
|
line String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
run ShapeImportRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([runId, seq])
|
|
@@map("shape_import_logs")
|
|
}
|
|
|
|
model NotificationPreference {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
notificationType String @map("notification_type")
|
|
inApp Boolean @default(true) @map("in_app")
|
|
email Boolean @default(false)
|
|
teams Boolean @default(false)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, notificationType])
|
|
@@map("notification_preferences")
|
|
}
|