seubert-claims/ondeck/prisma/schema.prisma
lorentz 6dd7e3e836 fix(tasks): timezone consistency, task provenance, setup N/A, and task-generation fixes
Timezone (all users EST):
- lib/utils: hardcode APP_TIME_ZONE (America/New_York) in formatDate/
  formatRenewalDate, add formatDateTime and todayInAppTimeZone helpers
- Replace every ad-hoc toLocaleDateString/toLocaleString call across the app
  (task notes, audit log, backups, shape import, renewal groups, client
  detail) with the shared EST-aware helpers
- Fix UTC "today" bug in date-input defaults/min/max (completion date,
  reminder date) that rolled to the next calendar day after ~7-8pm ET

Task provenance:
- Task card info icon (now visible to all users, not just privileged) shows
  full origin: template, level, renewal anchor, due-date math, and who/what
  generated the task
- Include template.daysOffset and creator in task queries

Setup N/A:
- New setupNaAt/setupNaReason fields on Client; mark/restore UI and API to
  exclude non-Shape/lost-business clients from the setup queue everywhere
  it's counted (queue page, API, manager dashboard, metrics gauge)

Task generation fixes:
- auto-generate: client-level branch now gated on the renewal anchor's
  (group/policy) createdAt instead of the client's, so pre-go-live clients
  with new post-go-live groups are no longer skipped forever
- New auto-assign-tasks sweep run after every sync to assign advocates to
  tasks created by paths that don't assign directly (e.g. setup wizard)
2026-07-16 22:32:04 +00:00

582 lines
21 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")
setupNaAt DateTime? @map("setup_na_at")
setupNaReason String? @map("setup_na_reason")
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
}
/// Two-pass audit classification, per imageright_claim_review_audit_methodology.md.
/// Separates substantive filing compliance from filename/naming-convention compliance.
enum TaskAuditClassification {
PASS
PASS_WITH_NAMING_EXCEPTION
MANUAL_REVIEW
FAIL_MISSING
FAIL_LATE
FAIL_WRONG_ARTIFACT
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")
classification TaskAuditClassification
/// 0-100 metadata confidence score (audit-scoring.ts); null when short-circuited (e.g. no file found).
confidenceScore Int? @map("confidence_score")
/// "high" | "medium" | "low" | "none" — bucketed from confidenceScore.
evidenceConfidence String? @map("evidence_confidence")
/// Did the matched artifact's own filename/type match the spec's expected pattern exactly?
filenamePatternCompliant Boolean? @map("filename_pattern_compliant")
/// True when classification is PASS_WITH_NAMING_EXCEPTION.
namingException Boolean? @map("naming_exception")
manualReviewRequired Boolean? @map("manual_review_required")
matchedDocId String? @map("matched_doc_id")
matchedDocName String? @map("matched_doc_name")
matchedDocDate DateTime? @map("matched_doc_date")
/// Subject of the .msg, when the matched artifact (or its container) was an email.
msgSubject String? @map("msg_subject")
msgSentDate DateTime? @map("msg_sent_date")
/// Filenames of attachments found inside a matched .msg container.
attachmentFilenames String[] @map("attachment_filenames")
/// Human-readable summary of what content evidence was found and why (for the audit report).
contentEvidenceSummary String? @map("content_evidence_summary") @db.Text
/// Ordered list of {signal, score, detail} scoring reasons, for the audit trail.
matchedSignals Json? @map("matched_signals")
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")
}