all the haul

This commit is contained in:
2026-04-07 23:56:31 +00:00
parent 87cce83030
commit 24f303355b
244 changed files with 33743 additions and 11249 deletions
+793 -25
View File
@@ -27,6 +27,18 @@ enum FaxType {
SITE // Main site fax line, not direct to contact
}
enum BillingMethod {
ACTUAL_RATES
FIXED_FEE
NOT_TO_EXCEED // Requires "Bill Ticket Seperately"
OVERRIDE_RATE // Shows hourly rate field
}
enum BillingType {
STANDARD
PROJECT
}
// By human nature, there are only two genders.
enum GenderType {
MALE
@@ -96,6 +108,59 @@ enum OpportunityInterest {
COLD
}
// ---- Sync Job Tracking ----
enum SyncJobType {
FULL_SYNC
INCREMENTAL_SYNC
}
enum SyncJobStatus {
QUEUED
RUNNING
COMPLETED
FAILED
TIMED_OUT
}
model SyncJobRun {
id String @id @default(uuid())
jobType SyncJobType
status SyncJobStatus @default(QUEUED)
triggeredBy String @default("system")
startedAt DateTime?
completedAt DateTime?
errorSummary String?
steps SyncStepLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SyncStepLog {
id String @id @default(uuid())
syncJobRunId String
syncJobRun SyncJobRun @relation(fields: [syncJobRunId], references: [id], onDelete: Cascade)
tableName String
syncMode String // "full" or "incremental"
recordsProcessed Int @default(0)
recordsInserted Int @default(0)
recordsSkipped Int @default(0)
recordsFailed Int @default(0)
recordsDeleted Int @default(0)
sampleErrors Json @default("[]")
durationMs Int @default(0)
createdAt DateTime @default(now())
}
model Session {
id String @id @default(uuid())
sessionKey String @unique @default(cuid())
@@ -108,21 +173,25 @@ model Session {
}
model User {
id String @id @default(cuid())
roles Role[]
permissions String?
login String @unique
name String?
email String @unique
emailVerified DateTime?
image String?
id String @id @default(uuid())
roles Role[]
permissions String?
login String @unique
firstName String?
lastName String?
email String @unique
image String?
title String?
active Boolean @default(true)
hidden Boolean @default(false)
cwIdentifier String? @unique
cwIdentifier String? @unique
cwMemberId Int? @unique
cwMember CwMember? @relation(fields: [cwMemberId], references: [cwMemberId])
userId String @unique
userId String? @unique
token String?
sessions Session[]
@@ -136,8 +205,16 @@ model User {
generatedQuotes GeneratedQuotes[]
companyAddresses CompanyAddress[]
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
serviceTicketsOwned ServiceTicket[] @relation("ServiceTicketOwner")
serviceTicketsClosed ServiceTicket[] @relation("ServiceTicketClosedBy")
serviceTicketsCreated ServiceTicket[] @relation("ServiceTicketCreatedBy")
serviceTicketsUpdated ServiceTicket[] @relation("ServiceTicketUpdatedBy")
serviceTicketNotes ServiceTicketNote[] @relation("ServiceTicketNoteAuthor")
}
model Role {
@@ -170,7 +247,9 @@ model CorporateLocation {
inactiveFlag Boolean @default(false) // Optima Only field, not synced to CW
opportunities Opportunity[]
opportunities Opportunity[]
serviceBoards ServiceTicketBoard[]
productDataRecords ProductData[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -232,11 +311,13 @@ model Company {
deletedBy User? @relation("DeletedBy", fields: [deletedById], references: [cwIdentifier])
enteredBy User? @relation("EnteredBy", fields: [enteredById], references: [cwIdentifier])
enteredAt DateTime @default(now())
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
serviceTickets ServiceTicket[]
billingServiceTickets ServiceTicket[] @relation("BillingCompany")
}
model CompanyAddress {
@@ -270,6 +351,8 @@ model CompanyAddress {
contacts Contact[]
oppportunities Opportunity[]
serviceTickets ServiceTicket[]
billingTickets ServiceTicket[] @relation("BillingAddress")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -304,29 +387,190 @@ model Contact {
company Company? @relation(fields: [companyId], references: [id])
opportunities Opportunity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
serviceTickets ServiceTicket[]
}
model CatalogItemType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogCategory {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
catalogSubcategories CatalogSubcategory[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogSubcategory {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
categoryId Int
category CatalogCategory @relation(fields: [categoryId], references: [id])
items CatalogItem[]
inactiveFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogManufacturer {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
items CatalogItem[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model WarehouseBin {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
minQuantity Int
maxQuantity Int
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
proeductInventories ProductInventory[]
warehouse Warehouse? @relation(fields: [warehouseId], references: [id])
warehouseId Int?
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ProductInventory {
id Int @unique
uid String @id @default(uuid())
qtyOnHand Int @default(0)
warehouseBinId Int
warehouseBin WarehouseBin @relation(fields: [warehouseBinId], references: [id])
itemId Int?
item CatalogItem? @relation(fields: [itemId], references: [id])
warehouse Warehouse? @relation(fields: [warehouseId], references: [id])
warehouseId Int?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Warehouse {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
lockedFlag Boolean @default(false)
minimumStock MinimumStockByWarehouse[]
inventory ProductInventory[]
bins WarehouseBin[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MinimumStockByWarehouse {
id Int @unique
uid String @id @default(uuid())
minQty Int @default(0)
warehouseId Int
warehouse Warehouse @relation(fields: [warehouseId], references: [id])
itemId Int?
item CatalogItem? @relation(fields: [itemId], references: [id])
updatedById String?
enteredById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CatalogItem {
id String @id @default(cuid())
cwCatalogId Int @unique
identifier String? @unique
name String
description String?
id Int @unique
uid String @id @default(uuid())
identifier String? @unique
name String
description String?
customerDescription String?
internalNotes String?
linkedItems CatalogItem[] @relation("LinkedItems")
linkedTo CatalogItem[] @relation("LinkedItems")
category String?
categoryCwId Int?
subcategory String?
subcategoryCwId Int?
subcategoryId Int
subcategory CatalogSubcategory @relation(fields: [subcategoryId], references: [id])
manufacturer String?
manufactureCwId Int?
manufacturerId Int?
manufacturer CatalogManufacturer? @relation(fields: [manufacturerId], references: [id])
partNumber String?
@@ -337,12 +581,403 @@ model CatalogItem {
price Float
cost Float
inventory ProductInventory[]
minimumStockByWarehouses MinimumStockByWarehouse[]
productDataRecords ProductData[]
inactive Boolean @default(false)
salesTaxable Boolean @default(true)
onHand Int @default(0)
cwLastUpdated DateTime?
// IV_Class_ID from CW: 'S' = Service/Labor, 'I' = Inventory, 'N' = Non-Inventory
classId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ProductData {
id Int @unique
uid String @id @default(uuid())
qty Float @default(1)
internalNote String?
shortDescription String?
description String?
sequenceNumber Int? // This is the sequence number of the product on the ticket, which may be different from the sequence number of the product in the catalog, and is important for maintaining the order of products on the ticket as they were added.
procurementNotes String?
productNarrative String?
unitPrice Float @default(0)
unitCost Float @default(0)
listPrice Float @default(0) // The original price of the product before any discounts or promotions are applied, which may be different from the unit price if there are any overrides or promotions applied at the ticket level.
discount Float @default(0) // The discount amount applied to this product, which may be different from the discount on the catalog item if there are any overrides or promotions applied at the ticket level.
recurringRevenue Float @default(0) // For subscription products, the recurring revenue amount, which may be different from the unit price if there are any discounts or promotions applied.
recurringCost Float @default(0) // For subscription products, the recurring cost amount, which may be different from the unit cost if there are any discounts or promotions applied.
qtyPicked Int @default(0) // How many of this product have been taken out of inventory
qtyShipped Int @default(0) // How many of this product have been recieved by the customer
cancelReason String? // If this product was canceled or removed from the ticket after being added, what was the reason for that?
cancelQty Float? // If this product was canceled or removed from the ticket after being added, how many were canceled or removed?
// ------ Flag Fields ------
billableFlag Boolean @default(true)
taxableFlag Boolean @default(true)
invoiceFlag Boolean @default(true)
recurringFlag Boolean @default(false) // Is this product a subscription or recurring revenue product?
poApprovedFlag Boolean @default(false) // Was the purchase of this product approved as part of a purchase order process?
calcPriceFlag Boolean @default(true) // Should the price of this product be calculated based on the catalog item price and any overrides, or should it use the unitPrice as is?
calcCostFlag Boolean @default(true) // Should the cost of this product be calculated based on the catalog item cost and any overrides, or should it use the unitCost as is?
cancelFlag Boolean @default(false) // Has this product been canceled or removed from the ticket after being added, but we want to keep the record of it for historical and reporting purposes?
// ------ Relational Fields ------
catalogItemId Int
corporateLocationId Int
serviceTicketId Int?
opportunityId Int?
serviceTicket ServiceTicket? @relation(fields: [serviceTicketId], references: [id])
opportunity Opportunity? @relation(fields: [opportunityId], references: [id])
catalogItem CatalogItem @relation(fields: [catalogItemId], references: [id])
corporateLocation CorporateLocation @relation(fields: [corporateLocationId], references: [id])
updatedById String?
createdById String?
closedById String?
cancelById String
closedAt DateTime?
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
//
// SERVICE TICKETS
//
model ServiceTicket {
id Int @unique
uid String @id @default(uuid())
// ------ Core ticket fields ------
summary String
notes ServiceTicketNote[]
addressLine1 String?
addressLine2 String?
city String?
state USState?
zipCode String?
country Country?
contactName String?
phone String?
phoneExtension String?
phoneType PhoneType?
email String?
// ------ Billing and invoicing fields ------
products ProductData[] // The products used on this ticket, which may be important for billing and invoicing, as well as reporting and analytics.
poNumber String?
billCompleteFlag Boolean @default(false) // Bill after ticket is closed, not allowing any billing while open.
billUnapprovedFlag Boolean @default(false) // Can this ticket bill unapproved work or expenses?
billingAmount Float @default(0.00)
billingMethod BillingMethod @default(ACTUAL_RATES)
timeBillableFlag Boolean @default(true) // Is the time spent on this ticket billable?
expenseBillableFlag Boolean @default(true) // Are the expenses incurred on this ticket billable?
productBillableFlag Boolean @default(true) // Are the products used on this ticket billable?
timeInvoiceableFlag Boolean @default(true) // Should the billable time on this ticket be included on invoices?
expenseInvoiceableFlag Boolean @default(true) // Should the billable expenses on this ticket be included on invoices?
productInvoiceableFlag Boolean @default(true) // Should the billable products on this ticket be included on invoices?
dateRequested DateTime? // The date the customer requested service, which may be different from the date the ticket was created in the system.
billingType BillingType @default(STANDARD) // (CUSTOM FIELD) Standard billing or project billing, which may have different rules for how the ticket is billed.
billingInstructions String? // (CUSTOM FIELD) Any special instructions for billing this ticket, which may be important for project billing or non-standard billing arrangements.
// ------ Flag Fields ------
rejectedFlag Boolean @default(false) // More used for denoting if a ticket was rejected by some automation.
closedFlag Boolean @default(false)
redFlag Boolean @default(false) // Carry over from CW, used for visibility and filtering.
publishFlag Boolean @default(false) // Should this ticket be visible to the customer in the portal or any other means?
// ------ Relational Fields ------
ticketOwnerId String?
serviceTicketBoardId Int?
severityId Int
impactId Int
priorityId Int
sourceId Int
locationId Int
parentId Int?
companyId Int?
contactId Int?
companyAddressId Int?
billingCompanyId Int?
billingAddressId Int?
severity ServiceTicketSeverity @relation(fields: [severityId], references: [id])
impact ServiceTicketImpact @relation(fields: [impactId], references: [id])
priority ServiceTicketPriority @relation(fields: [priorityId], references: [id])
source ServiceTicketSource @relation(fields: [sourceId], references: [id])
location ServiceTicketLocation @relation(fields: [locationId], references: [id])
serviceTicketBoard ServiceTicketBoard? @relation(fields: [serviceTicketBoardId], references: [id])
ticketOwner User? @relation("ServiceTicketOwner", fields: [ticketOwnerId], references: [cwIdentifier])
company Company? @relation(fields: [companyId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
companyAddress CompanyAddress? @relation(fields: [companyAddressId], references: [id])
billingCompany Company? @relation("BillingCompany", fields: [billingCompanyId], references: [id])
billingAddress CompanyAddress? @relation("BillingAddress", fields: [billingAddressId], references: [id])
// ------ Audit Fields ------
createdById String?
updatedById String?
closedById String?
closedBy User? @relation("ServiceTicketClosedBy", fields: [closedById], references: [id])
createdBy User? @relation("ServiceTicketCreatedBy", fields: [createdById], references: [id])
updatedBy User? @relation("ServiceTicketUpdatedBy", fields: [updatedById], references: [id])
rejectedAt DateTime?
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketNote {
id Int @unique
uid String @id @default(uuid())
notes String
notesMd String
authorId String
author User @relation("ServiceTicketNoteAuthor", fields: [authorId], references: [id])
problemFlag Boolean @default(false) // Is this note describing the problem?
resolutionFlag Boolean @default(false) // Is this note describing the resolution?
internalAnalysisFlag Boolean @default(false) // Is this note describing the internal analysis of the issue, such as root cause analysis or technical details that may not be relevant to the customer?
internalMemberFlag Boolean @default(false) // Is this note meant to be seen by internal team members only, not visible to the customer?
createdByParentFlag Boolean @default(false) // Is this note created by the parent entity.
mergedFlag Boolean @default(false)
bundledFlag Boolean @default(false)
serviceTicketId Int
serviceTicket ServiceTicket @relation(fields: [serviceTicketId], references: [id])
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketBoard {
id Int @unique
uid String @id @default(uuid())
name String
// Does this generate revenue for the company?
timeBillableFlag Boolean @default(false)
expenseBillableFlag Boolean @default(false)
productBillableFlag Boolean @default(false)
// Can the customer see this on their invoice?
timeInvoiceableFlag Boolean @default(false)
expenseInvoiceableFlag Boolean @default(false)
productInvoiceableFlag Boolean @default(false)
// These are auto assignment rule flags
// If/When I implement a system for auto assignement,
// these flags will determine which fields are considered
// for auto assignment when a ticket is created without an assignee.
// These are a carry over from CW.
autoAssignNewFlag Boolean @default(false)
autoAssignEmailCreatedFlag Boolean @default(false)
autoAssignPortalCreatedFlag Boolean @default(false)
projectFlag Boolean @default(false)
lockDescriptionFlag Boolean @default(false) // Should we lock the description field on tickets in this board after creation?
emailContactFlag Boolean @default(false) // Should we email the contact when a ticket is updated?
emailResourceFlag Boolean @default(false) // Should we email the assigned resource(s) when a ticket is updated?
resolutionSortOrder String @default("D") @db.Char(1)
internalAnalysisSortOrder String @default("D") @db.Char(1)
locationId Int
location CorporateLocation @relation(fields: [locationId], references: [id])
serviceTickets ServiceTicket[]
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketLocation {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ServiceTicketSource {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
inactiveFlag Boolean @default(false)
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How much does the issue affect the customer or their business operations
model ServiceTicketImpact {
id Int @unique
uid String @id @default(uuid())
name String
description String?
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How soon does the issue need to be addressed/resolved
model ServiceTicketPriority {
id Int @unique
uid String @id @default(uuid())
name String
color String? // Hex color code for priority, e.g. "#FF0000" for red
description String? // Optima Only field, not synced to CW
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// How bad is the Technical Issue
model ServiceTicketSeverity {
id Int @unique
uid String @id @default(uuid())
name String
description String?
defaultFlag Boolean @default(false)
serviceTickets ServiceTicket[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("ServiceTicketServerity")
}
// This data is populated asynchronously after a ticket is closed,
// so we can keep the critical path of closing a ticket fast and not
// dependent on any additional data processing. This will also allow
// us to be able to store the data AS IT was at the time of closing,
// without worrying about any additional updates that may come in after the fact.
model ServiceTicketFinalData {
id String @id @default(uuid())
}
model OpportunityStage {
id Int @unique
uid String @id @default(uuid())
name String
seqNbr Int?
funnelColor String?
updatedById String?
opportunities Opportunity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -393,17 +1028,22 @@ model Opportunity {
name String
notes String?
oppNarrative String? // A long form text field for the story of the opportunity, which may include details about the customer's needs, the proposed solution, and any other relevant information that doesn't fit into the structured fields.
generatedQuotes GeneratedQuotes[]
typeId Int
type OpportunityType @relation(fields: [typeId], references: [id])
stageName String?
stageCwId Int?
stageId Int?
stage OpportunityStage? @relation(fields: [stageId], references: [id])
statusId Int?
status OpportunityStatus? @relation(fields: [statusId], references: [id])
taxCodeId Int?
taxCode TaxCode? @relation(fields: [taxCodeId], references: [id])
interest OpportunityInterest?
probability Float @default(0)
@@ -447,6 +1087,111 @@ model Opportunity {
// When present, fetchProducts() uses this order instead of CW sequenceNumber.
productSequence Int[] @default([])
products ProductData[]
updatedBy String
eneteredBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------ Schedule / Calendar --------
model ScheduleStatus {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
color String?
softFlag Boolean @default(false)
defaultFlag Boolean @default(false)
schedules Schedule[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ScheduleType {
id Int @unique
uid String @id @default(uuid())
name String
description String? // Optima Only field, not synced to CW
displayColor String?
tableReference String?
moduleId String? @db.Char(2)
scheduleTypeId String? @db.Char(1)
systemFlag Boolean @default(false)
displayFlag Boolean @default(false)
schedules Schedule[]
updatedById String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ScheduleSpan {
id Int @id @default(autoincrement())
scheduleSpanId String? @db.Char(1)
spanDesc String? @db.Char(20)
schedules Schedule[]
}
model Schedule {
id Int @unique
uid String @id @default(uuid())
name String
description String?
memberId String?
closedFlag Boolean @default(false)
reminderFlag Boolean @default(false)
allDayFlag Boolean @default(false)
acknowledgementFlag Boolean @default(false)
meetingFlag Boolean @default(false)
recurringFlag Boolean @default(false)
billableFlag Boolean @default(false)
acknowledgedById String?
acknowledgedAt DateTime?
startDate DateTime?
endDate DateTime?
hoursScheduled Float?
duration Int? // The number of days in between the start and end date.
hoursPerDay Float?
reminderMinutes Int? @default(15)
statusId Int?
status ScheduleStatus? @relation(fields: [statusId], references: [id])
typeId Int?
type ScheduleType? @relation(fields: [typeId], references: [id])
scheduleSpanId Int?
scheduleSpan ScheduleSpan? @relation(fields: [scheduleSpanId], references: [id])
updatedById String?
createdById String?
closedById String?
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -523,6 +1268,27 @@ model GeneratedQuotes {
updatedAt DateTime @updatedAt
}
model TaxCode {
id Int @unique
uid String @id @default(uuid())
opportunities Opportunity[]
code String @unique
codeCaption String
description String?
rate Float?
defaultFlag Boolean @default(false)
createdBy String
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CwMember {
id String @id @default(cuid())
@@ -535,6 +1301,8 @@ model CwMember {
apiKey String?
user User?
cwLastUpdated DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt