seubert-claims/ondeck/prisma/schema.prisma
lorentz fad9e18662 feat(imageright): Task Audit feature for SHAPE clients
- 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.
2026-07-08 10:39:12 +00:00

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")
}