schema.prisma 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419
  1. generator client {
  2. provider = "prisma-client"
  3. output = "../src/generated/prisma"
  4. }
  5. datasource db {
  6. provider = "postgresql"
  7. }
  8. model User {
  9. id String @id @default(cuid())
  10. name String
  11. email String @unique
  12. emailVerified Boolean @default(false)
  13. image String?
  14. isSuperAdmin Boolean @default(false)
  15. createdAt DateTime @default(now())
  16. updatedAt DateTime @updatedAt
  17. twoFactorEnabled Boolean @default(false)
  18. termsAcceptedAt DateTime?
  19. lastLogin DateTime?
  20. lastSeen DateTime?
  21. /// App version this user last saw/dismissed the update banner for.
  22. /// Null = never seeded (seeded silently on first load, no banner).
  23. lastSeenVersion String?
  24. /// Per-user dashboard grid layout (card positions/sizes/hidden set).
  25. /// Null = default layout. Shape validated in dashboardLayoutActions.
  26. dashboardLayout Json?
  27. sessions Session[]
  28. accounts Account[]
  29. vehicles Vehicle[]
  30. customers Customer[]
  31. settings AppSetting[]
  32. inventoryParts InventoryPart[]
  33. quotes Quote[]
  34. customFieldDefinitions CustomFieldDefinition[]
  35. twoFactor TwoFactor?
  36. passkeys Passkey[]
  37. organizationMembers OrganizationMember[]
  38. dashboardWidgets DashboardWidget[]
  39. auditLogs AuditLog[]
  40. technicians Technician[]
  41. stockMovements StockMovement[]
  42. @@map("users")
  43. }
  44. model AppSetting {
  45. id String @id @default(cuid())
  46. key String
  47. value String
  48. userId String
  49. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  50. organizationId String?
  51. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  52. @@unique([organizationId, key])
  53. @@index([organizationId])
  54. @@map("app_settings")
  55. }
  56. model Session {
  57. id String @id @default(cuid())
  58. expiresAt DateTime
  59. token String @unique
  60. createdAt DateTime @default(now())
  61. updatedAt DateTime @updatedAt
  62. ipAddress String?
  63. userAgent String?
  64. userId String
  65. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  66. @@index([userId])
  67. @@map("sessions")
  68. }
  69. model Account {
  70. id String @id @default(cuid())
  71. accountId String
  72. providerId String
  73. userId String
  74. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  75. accessToken String?
  76. refreshToken String?
  77. idToken String?
  78. accessTokenExpiresAt DateTime?
  79. refreshTokenExpiresAt DateTime?
  80. scope String?
  81. password String?
  82. createdAt DateTime @default(now())
  83. updatedAt DateTime @updatedAt
  84. @@index([userId])
  85. @@map("accounts")
  86. }
  87. model Verification {
  88. id String @id @default(cuid())
  89. identifier String @unique
  90. value String
  91. expiresAt DateTime
  92. createdAt DateTime? @default(now())
  93. updatedAt DateTime? @updatedAt
  94. @@map("verifications")
  95. }
  96. model TwoFactor {
  97. id String @id @default(cuid())
  98. secret String
  99. backupCodes String
  100. userId String @unique
  101. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  102. @@map("two_factor")
  103. }
  104. model Passkey {
  105. id String @id @default(cuid())
  106. name String?
  107. publicKey String
  108. userId String
  109. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  110. credentialID String @unique
  111. counter Int
  112. deviceType String
  113. backedUp Boolean
  114. transports String?
  115. aaguid String?
  116. createdAt DateTime @default(now())
  117. @@index([userId])
  118. @@map("passkeys")
  119. }
  120. model Vehicle {
  121. id String @id @default(cuid())
  122. make String
  123. model String
  124. year Int
  125. vin String?
  126. licensePlate String?
  127. color String?
  128. mileage Int @default(0)
  129. fuelType String? @default("gasoline")
  130. transmission String? @default("automatic")
  131. engineSize String?
  132. engineCode String?
  133. purchaseDate DateTime?
  134. purchasePrice Float?
  135. imageUrl String?
  136. isArchived Boolean @default(false)
  137. archiveReason String?
  138. maintenanceDismissed Boolean @default(false)
  139. maintenanceDismissedAt DateTime?
  140. aiMessages AiGeneratedMessage[]
  141. createdAt DateTime @default(now())
  142. updatedAt DateTime @updatedAt
  143. userId String
  144. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  145. organizationId String?
  146. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  147. customerId String?
  148. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  149. serviceRecords ServiceRecord[]
  150. notes Note[]
  151. fuelLogs FuelLog[]
  152. reminders Reminder[]
  153. quotes Quote[]
  154. recurringInvoices RecurringInvoice[]
  155. inspections Inspection[]
  156. serviceRequests ServiceRequest[]
  157. findings VehicleFinding[]
  158. @@index([organizationId])
  159. @@map("vehicles")
  160. }
  161. model ServiceRecord {
  162. id String @id @default(cuid())
  163. title String
  164. description String?
  165. type String @default("maintenance")
  166. status String @default("completed")
  167. cost Float @default(0)
  168. mileage Int?
  169. serviceDate DateTime @default(now())
  170. startDateTime DateTime?
  171. endDateTime DateTime?
  172. shopName String?
  173. techName String?
  174. parts String?
  175. laborHours Float?
  176. diagnosticNotes String?
  177. invoiceNotes String?
  178. subtotal Float @default(0)
  179. taxRate Float @default(0)
  180. taxAmount Float @default(0)
  181. taxInclusive Boolean @default(false)
  182. totalAmount Float @default(0)
  183. invoiceNumber String?
  184. invoiceDate DateTime?
  185. invoiceDueDate DateTime?
  186. discountType String?
  187. discountValue Float @default(0)
  188. discountAmount Float @default(0)
  189. manuallyPaid Boolean @default(false)
  190. // Warranty tracking
  191. warrantyMonths Int?
  192. warrantyMileage Int?
  193. warrantyExpiresAt DateTime?
  194. warrantyNotes String?
  195. publicToken String? @unique
  196. sharedAt DateTime?
  197. viewCount Int @default(0)
  198. lastViewedAt DateTime?
  199. createdAt DateTime @default(now())
  200. updatedAt DateTime @updatedAt
  201. /// Null for parts-only / counter sales; such records use customerId
  202. /// (or neither, for anonymous walk-in sales)
  203. vehicleId String?
  204. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  205. /// Direct customer for records without a vehicle. Vehicle-linked records
  206. /// leave this null and resolve the customer through the vehicle, so
  207. /// reassigning a vehicle keeps invoices following the vehicle's customer.
  208. customerId String?
  209. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  210. /// Denormalized from the vehicle historically; now the canonical scope so
  211. /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
  212. organizationId String?
  213. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  214. technicianId String?
  215. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  216. sortOrder Int @default(0)
  217. partItems ServicePart[]
  218. laborItems ServiceLabor[]
  219. attachments ServiceAttachment[]
  220. payments Payment[]
  221. statusReports StatusReport[]
  222. stockMovements StockMovement[]
  223. discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
  224. resolvedFindings VehicleFinding[] @relation("FindingResolvedIn")
  225. @@index([vehicleId])
  226. @@index([technicianId])
  227. @@index([organizationId])
  228. @@index([customerId])
  229. @@map("service_records")
  230. }
  231. model Note {
  232. id String @id @default(cuid())
  233. title String
  234. content String
  235. isPinned Boolean @default(false)
  236. createdAt DateTime @default(now())
  237. updatedAt DateTime @updatedAt
  238. vehicleId String
  239. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  240. @@index([vehicleId])
  241. @@map("notes")
  242. }
  243. model FuelLog {
  244. id String @id @default(cuid())
  245. date DateTime @default(now())
  246. mileage Int
  247. gallons Float
  248. pricePerGallon Float
  249. totalCost Float
  250. isFillUp Boolean @default(true)
  251. station String?
  252. notes String?
  253. createdAt DateTime @default(now())
  254. updatedAt DateTime @updatedAt
  255. vehicleId String
  256. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  257. @@index([vehicleId])
  258. @@map("fuel_logs")
  259. }
  260. model Reminder {
  261. id String @id @default(cuid())
  262. title String
  263. description String?
  264. dueDate DateTime?
  265. dueMileage Int?
  266. isCompleted Boolean @default(false)
  267. /// Per-reminder notification channels, chosen when creating the reminder
  268. notifyInApp Boolean @default(true)
  269. notifyEmail Boolean @default(false)
  270. /// Set by the due-reminder cron once notified; cleared when dueDate changes
  271. notifiedAt DateTime?
  272. createdAt DateTime @default(now())
  273. updatedAt DateTime @updatedAt
  274. /// A reminder relates to a vehicle, a customer, or just the workshop
  275. /// (both null). organizationId is the canonical scope for all three.
  276. vehicleId String?
  277. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  278. customerId String?
  279. customer Customer? @relation(fields: [customerId], references: [id], onDelete: Cascade)
  280. organizationId String?
  281. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  282. @@index([vehicleId])
  283. @@index([customerId])
  284. @@index([organizationId])
  285. @@map("reminders")
  286. }
  287. model VehicleFinding {
  288. id String @id @default(cuid())
  289. description String
  290. severity String @default("needs_work")
  291. status String @default("open")
  292. notes String?
  293. imageUrls String[] @default([])
  294. createdAt DateTime @default(now())
  295. updatedAt DateTime @updatedAt
  296. vehicleId String
  297. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  298. serviceRecordId String?
  299. serviceRecord ServiceRecord? @relation("FindingDiscoveredIn", fields: [serviceRecordId], references: [id], onDelete: SetNull)
  300. resolvedServiceRecordId String?
  301. resolvedServiceRecord ServiceRecord? @relation("FindingResolvedIn", fields: [resolvedServiceRecordId], references: [id], onDelete: SetNull)
  302. @@index([vehicleId])
  303. @@index([serviceRecordId])
  304. @@map("vehicle_findings")
  305. }
  306. model Customer {
  307. id String @id @default(cuid())
  308. /// Human-facing customer number: auto-assigned sequentially on create,
  309. /// editable afterwards. Unique per organization (nulls allowed for
  310. /// legacy customers).
  311. customerNumber String?
  312. name String
  313. email String?
  314. phone String?
  315. address String?
  316. company String?
  317. taxId String?
  318. taxExempt Boolean @default(false)
  319. notes String?
  320. telegramChatId String?
  321. createdAt DateTime @default(now())
  322. updatedAt DateTime @updatedAt
  323. userId String
  324. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  325. organizationId String?
  326. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  327. vehicles Vehicle[]
  328. quotes Quote[]
  329. smsMessages SmsMessage[]
  330. telegramMessages TelegramMessage[]
  331. customerSessions CustomerSession[]
  332. serviceRequests ServiceRequest[]
  333. serviceRecords ServiceRecord[]
  334. reminders Reminder[]
  335. @@unique([organizationId, customerNumber])
  336. @@index([organizationId])
  337. @@map("customers")
  338. }
  339. model Payment {
  340. id String @id @default(cuid())
  341. amount Float
  342. date DateTime @default(now())
  343. method String @default("other")
  344. note String?
  345. provider String?
  346. externalId String?
  347. createdAt DateTime @default(now())
  348. updatedAt DateTime @updatedAt
  349. serviceRecordId String
  350. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  351. @@index([serviceRecordId])
  352. @@map("payments")
  353. }
  354. model ServiceAttachment {
  355. id String @id @default(cuid())
  356. fileName String
  357. fileUrl String
  358. fileType String
  359. fileSize Int
  360. category String @default("diagnostic")
  361. description String?
  362. includeInInvoice Boolean @default(true)
  363. createdAt DateTime @default(now())
  364. serviceRecordId String
  365. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  366. @@index([serviceRecordId])
  367. @@map("service_attachments")
  368. }
  369. model ServicePart {
  370. id String @id @default(cuid())
  371. partNumber String?
  372. name String
  373. quantity Float @default(1)
  374. unitPrice Float @default(0)
  375. total Float @default(0)
  376. unitCost Float @default(0)
  377. markupPercent Float @default(0)
  378. inventoryPartId String?
  379. serviceRecordId String
  380. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  381. @@index([serviceRecordId])
  382. @@map("service_parts")
  383. }
  384. model ServiceLabor {
  385. id String @id @default(cuid())
  386. description String
  387. hours Float @default(0)
  388. rate Float @default(0)
  389. total Float @default(0)
  390. pricingType String @default("hourly")
  391. serviceRecordId String
  392. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  393. @@index([serviceRecordId])
  394. @@map("service_labor")
  395. }
  396. model LaborPreset {
  397. id String @id @default(cuid())
  398. name String
  399. description String?
  400. isArchived Boolean @default(false)
  401. createdAt DateTime @default(now())
  402. updatedAt DateTime @updatedAt
  403. userId String
  404. organizationId String
  405. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  406. items LaborPresetItem[]
  407. parts LaborPresetPart[]
  408. @@index([organizationId])
  409. @@map("labor_presets")
  410. }
  411. model LaborPresetItem {
  412. id String @id @default(cuid())
  413. description String
  414. hours Float @default(0)
  415. rate Float @default(0)
  416. pricingType String @default("hourly")
  417. sortOrder Int @default(0)
  418. presetId String
  419. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  420. @@index([presetId])
  421. @@map("labor_preset_items")
  422. }
  423. model LaborPresetPart {
  424. id String @id @default(cuid())
  425. name String
  426. partNumber String?
  427. quantity Float @default(1)
  428. unitPrice Float @default(0)
  429. inventoryPartId String?
  430. sortOrder Int @default(0)
  431. presetId String
  432. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  433. @@index([presetId])
  434. @@map("labor_preset_parts")
  435. }
  436. model InventoryPart {
  437. id String @id @default(cuid())
  438. partNumber String?
  439. barcode String?
  440. name String
  441. description String?
  442. category String?
  443. quantity Int @default(0)
  444. minQuantity Int @default(0)
  445. unitCost Float @default(0)
  446. sellPrice Float @default(0)
  447. supplier String?
  448. supplierPhone String?
  449. supplierEmail String?
  450. supplierUrl String?
  451. imageUrl String?
  452. location String?
  453. isArchived Boolean @default(false)
  454. /// Set when a low-stock alert has been sent for the current dip below
  455. /// `minQuantity`, and cleared once stock climbs back above it. This
  456. /// hysteresis is what stops a part that sits at zero from re-alerting on
  457. /// every check — it alerts once per descent, not once per run.
  458. lowStockAlertedAt DateTime?
  459. createdAt DateTime @default(now())
  460. updatedAt DateTime @updatedAt
  461. userId String
  462. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  463. organizationId String?
  464. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  465. gallery StoredImage[] @relation("InventoryPartImages")
  466. movements StockMovement[]
  467. @@unique([organizationId, barcode])
  468. @@index([organizationId])
  469. @@map("inventory_parts")
  470. }
  471. /// Append-only audit trail of every change to InventoryPart.quantity.
  472. ///
  473. /// Written inside the same transaction as the stock mutation itself, so the
  474. /// ledger can never drift from the balance it describes. `quantityAfter` is
  475. /// captured from the atomic `UPDATE ... RETURNING`, giving an exact running
  476. /// balance even under concurrent writes.
  477. model StockMovement {
  478. id String @id @default(cuid())
  479. /// Signed change in whole units. Negative = consumed, positive = restocked.
  480. delta Int
  481. /// Part balance immediately after this movement was applied.
  482. quantityAfter Int
  483. /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
  484. reason String
  485. note String?
  486. inventoryPartId String
  487. inventoryPart InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
  488. /// The job the part was used on. Kept on SetNull so history survives
  489. /// deletion of the service record — "where was this used" outlives the job.
  490. serviceRecordId String?
  491. serviceRecord ServiceRecord? @relation(fields: [serviceRecordId], references: [id], onDelete: SetNull)
  492. /// Denormalised so history still reads correctly after a service record is
  493. /// deleted and serviceRecordId goes null.
  494. serviceRecordLabel String?
  495. userId String?
  496. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  497. organizationId String
  498. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  499. createdAt DateTime @default(now())
  500. @@index([inventoryPartId, createdAt(sort: Desc)])
  501. @@index([organizationId, createdAt(sort: Desc)])
  502. @@index([serviceRecordId])
  503. @@map("stock_movements")
  504. }
  505. model StoredImage {
  506. id String @id @default(cuid())
  507. url String
  508. fileName String?
  509. description String?
  510. sortOrder Int @default(0)
  511. createdAt DateTime @default(now())
  512. inventoryPartId String?
  513. inventoryPart InventoryPart? @relation("InventoryPartImages", fields: [inventoryPartId], references: [id], onDelete: Cascade)
  514. @@index([inventoryPartId])
  515. @@map("stored_images")
  516. }
  517. model Quote {
  518. id String @id @default(cuid())
  519. quoteNumber String?
  520. title String
  521. description String?
  522. status String @default("draft")
  523. validUntil DateTime?
  524. subtotal Float @default(0)
  525. taxRate Float @default(0)
  526. taxAmount Float @default(0)
  527. taxInclusive Boolean @default(false)
  528. discountType String?
  529. discountValue Float @default(0)
  530. discountAmount Float @default(0)
  531. totalAmount Float @default(0)
  532. notes String?
  533. publicToken String?
  534. sharedAt DateTime?
  535. viewCount Int @default(0)
  536. lastViewedAt DateTime?
  537. customerMessage String?
  538. convertedToId String?
  539. createdAt DateTime @default(now())
  540. updatedAt DateTime @updatedAt
  541. userId String
  542. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  543. organizationId String?
  544. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  545. customerId String?
  546. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  547. vehicleId String?
  548. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  549. inspectionId String?
  550. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  551. partItems QuotePart[]
  552. laborItems QuoteLabor[]
  553. attachments QuoteAttachment[]
  554. @@index([organizationId])
  555. @@map("quotes")
  556. }
  557. model QuoteAttachment {
  558. id String @id @default(cuid())
  559. fileName String
  560. fileUrl String
  561. fileType String
  562. fileSize Int
  563. category String @default("image")
  564. description String?
  565. includeInInvoice Boolean @default(true)
  566. createdAt DateTime @default(now())
  567. quoteId String
  568. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  569. @@index([quoteId])
  570. @@map("quote_attachments")
  571. }
  572. model QuotePart {
  573. id String @id @default(cuid())
  574. partNumber String?
  575. name String
  576. quantity Float @default(1)
  577. /// Internal purchase cost — never shown to customers (quote PDF/share
  578. /// render unitPrice only)
  579. unitCost Float @default(0)
  580. markupPercent Float @default(0)
  581. unitPrice Float @default(0)
  582. total Float @default(0)
  583. excluded Boolean @default(false)
  584. /// Optional link to the stocked part this line was picked from. Carried into
  585. /// ServicePart on conversion so the job deducts the right inventory item.
  586. inventoryPartId String?
  587. quoteId String
  588. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  589. @@index([quoteId])
  590. @@map("quote_parts")
  591. }
  592. model QuoteLabor {
  593. id String @id @default(cuid())
  594. description String
  595. hours Float @default(0)
  596. rate Float @default(0)
  597. total Float @default(0)
  598. pricingType String @default("hourly")
  599. excluded Boolean @default(false)
  600. quoteId String
  601. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  602. @@index([quoteId])
  603. @@map("quote_labor")
  604. }
  605. model CustomFieldDefinition {
  606. id String @id @default(cuid())
  607. name String
  608. label String
  609. fieldType String @default("text")
  610. options String?
  611. required Boolean @default(false)
  612. entityType String
  613. sortOrder Int @default(0)
  614. isActive Boolean @default(true)
  615. createdAt DateTime @default(now())
  616. updatedAt DateTime @updatedAt
  617. userId String
  618. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  619. organizationId String?
  620. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  621. values CustomFieldValue[]
  622. @@unique([organizationId, name, entityType])
  623. @@index([organizationId])
  624. @@map("custom_field_definitions")
  625. }
  626. model CustomFieldValue {
  627. id String @id @default(cuid())
  628. value String
  629. entityId String
  630. entityType String
  631. fieldId String
  632. field CustomFieldDefinition @relation(fields: [fieldId], references: [id], onDelete: Cascade)
  633. @@unique([fieldId, entityId])
  634. @@index([fieldId])
  635. @@map("custom_field_values")
  636. }
  637. /// A user-defined dashboard card: a named, filtered table over one entity.
  638. /// The config shape (entity, filters, columns, limit) is validated against
  639. /// the field registry in customCardActions — nothing here reaches SQL raw.
  640. model DashboardWidget {
  641. id String @id @default(cuid())
  642. name String
  643. config Json
  644. userId String
  645. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  646. organizationId String
  647. createdAt DateTime @default(now())
  648. updatedAt DateTime @updatedAt
  649. @@index([userId, organizationId])
  650. @@map("dashboard_widgets")
  651. }
  652. model Organization {
  653. id String @id @default(cuid())
  654. name String
  655. portalSlug String? @unique
  656. createdAt DateTime @default(now())
  657. updatedAt DateTime @updatedAt
  658. members OrganizationMember[]
  659. roles Role[]
  660. vehicles Vehicle[]
  661. serviceRecords ServiceRecord[]
  662. reminders Reminder[]
  663. customers Customer[]
  664. quotes Quote[]
  665. inventoryParts InventoryPart[]
  666. laborPresets LaborPreset[]
  667. customFieldDefinitions CustomFieldDefinition[]
  668. settings AppSetting[]
  669. subscription Subscription?
  670. invitations TeamInvitation[]
  671. inspectionTemplates InspectionTemplate[]
  672. smsMessages SmsMessage[]
  673. telegramMessages TelegramMessage[]
  674. technicians Technician[]
  675. auditLogs AuditLog[]
  676. statusReports StatusReport[]
  677. reportSchedules ReportSchedule[]
  678. webhooks Webhook[]
  679. stockMovements StockMovement[]
  680. @@map("organizations")
  681. }
  682. model OrganizationMember {
  683. id String @id @default(cuid())
  684. role String @default("member")
  685. userId String
  686. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  687. organizationId String
  688. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  689. roleId String?
  690. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  691. @@unique([userId, organizationId])
  692. @@map("organization_members")
  693. }
  694. model Role {
  695. id String @id @default(cuid())
  696. name String
  697. isAdmin Boolean @default(false)
  698. createdAt DateTime @default(now())
  699. updatedAt DateTime @updatedAt
  700. organizationId String
  701. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  702. permissions Permission[]
  703. members OrganizationMember[]
  704. teamInvitations TeamInvitation[]
  705. @@unique([organizationId, name])
  706. @@map("roles")
  707. }
  708. model Permission {
  709. id String @id @default(cuid())
  710. action String
  711. subject String
  712. roleId String
  713. role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
  714. @@unique([roleId, action, subject])
  715. @@index([roleId])
  716. @@map("permissions")
  717. }
  718. model SystemSetting {
  719. id String @id @default(cuid())
  720. key String @unique
  721. value String
  722. @@map("system_settings")
  723. }
  724. model TeamInvitation {
  725. id String @id @default(cuid())
  726. email String
  727. role String @default("member")
  728. token String @unique
  729. status String @default("pending") // pending | accepted | cancelled
  730. expiresAt DateTime
  731. createdAt DateTime @default(now())
  732. organizationId String
  733. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  734. invitedById String
  735. roleId String?
  736. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  737. @@unique([email, organizationId])
  738. @@index([token])
  739. @@map("team_invitations")
  740. }
  741. // Stripe-ready models (implementation in follow-up)
  742. model SubscriptionPlan {
  743. id String @id @default(cuid())
  744. name String
  745. stripePriceId String? @unique
  746. price Float @default(0)
  747. interval String @default("month") // "month" | "year"
  748. maxMembers Int @default(1)
  749. features String? // JSON feature flags
  750. isActive Boolean @default(true)
  751. createdAt DateTime @default(now())
  752. updatedAt DateTime @updatedAt
  753. subscriptions Subscription[]
  754. @@map("subscription_plans")
  755. }
  756. model Subscription {
  757. id String @id @default(cuid())
  758. status String @default("active") // active | canceled | past_due | trialing
  759. stripeSubscriptionId String? @unique
  760. stripeCustomerId String?
  761. currentPeriodStart DateTime?
  762. currentPeriodEnd DateTime?
  763. cancelAtPeriodEnd Boolean @default(false)
  764. createdAt DateTime @default(now())
  765. updatedAt DateTime @updatedAt
  766. organizationId String @unique
  767. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  768. planId String
  769. plan SubscriptionPlan @relation(fields: [planId], references: [id])
  770. @@map("subscriptions")
  771. }
  772. model RecurringInvoice {
  773. id String @id @default(cuid())
  774. title String
  775. description String?
  776. frequency String // "weekly" | "biweekly" | "monthly" | "quarterly" | "yearly"
  777. nextRunDate DateTime
  778. endDate DateTime?
  779. isActive Boolean @default(true)
  780. lastRunAt DateTime?
  781. runCount Int @default(0)
  782. // Template fields (copied to ServiceRecord on generation)
  783. type String @default("maintenance")
  784. cost Float @default(0)
  785. taxRate Float @default(0)
  786. taxInclusive Boolean @default(false)
  787. invoiceNotes String?
  788. vehicleId String
  789. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  790. createdAt DateTime @default(now())
  791. updatedAt DateTime @updatedAt
  792. templateParts RecurringPart[]
  793. templateLabor RecurringLabor[]
  794. @@index([vehicleId])
  795. @@index([nextRunDate, isActive])
  796. @@map("recurring_invoices")
  797. }
  798. model RecurringPart {
  799. id String @id @default(cuid())
  800. name String
  801. partNumber String?
  802. quantity Int @default(1)
  803. unitPrice Float @default(0)
  804. recurringInvoiceId String
  805. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  806. @@map("recurring_parts")
  807. }
  808. model RecurringLabor {
  809. id String @id @default(cuid())
  810. description String
  811. hours Float @default(0)
  812. rate Float @default(0)
  813. pricingType String @default("hourly")
  814. recurringInvoiceId String
  815. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  816. @@map("recurring_labor")
  817. }
  818. model InspectionTemplate {
  819. id String @id @default(cuid())
  820. name String
  821. description String?
  822. isDefault Boolean @default(false)
  823. createdAt DateTime @default(now())
  824. updatedAt DateTime @updatedAt
  825. organizationId String
  826. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  827. sections InspectionTemplateSection[]
  828. inspections Inspection[]
  829. @@index([organizationId])
  830. @@map("inspection_templates")
  831. }
  832. model InspectionTemplateSection {
  833. id String @id @default(cuid())
  834. name String
  835. sortOrder Int @default(0)
  836. templateId String
  837. template InspectionTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
  838. items InspectionTemplateItem[]
  839. @@index([templateId])
  840. @@map("inspection_template_sections")
  841. }
  842. model InspectionTemplateItem {
  843. id String @id @default(cuid())
  844. name String
  845. sortOrder Int @default(0)
  846. sectionId String
  847. section InspectionTemplateSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
  848. @@index([sectionId])
  849. @@map("inspection_template_items")
  850. }
  851. model Inspection {
  852. id String @id @default(cuid())
  853. status String @default("in_progress")
  854. mileage Int?
  855. notes String?
  856. startDateTime DateTime?
  857. endDateTime DateTime?
  858. publicToken String? @unique
  859. completedAt DateTime?
  860. createdAt DateTime @default(now())
  861. updatedAt DateTime @updatedAt
  862. vehicleId String
  863. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  864. templateId String
  865. template InspectionTemplate @relation(fields: [templateId], references: [id])
  866. technicianId String?
  867. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  868. sortOrder Int @default(0)
  869. organizationId String
  870. items InspectionItem[]
  871. quoteRequests InspectionQuoteRequest[]
  872. quotes Quote[]
  873. @@index([vehicleId])
  874. @@index([organizationId])
  875. @@index([technicianId])
  876. @@map("inspections")
  877. }
  878. model InspectionItem {
  879. id String @id @default(cuid())
  880. name String
  881. section String
  882. sortOrder Int @default(0)
  883. condition String @default("not_inspected")
  884. notes String?
  885. imageUrls String[] @default([])
  886. inspectionId String
  887. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  888. @@index([inspectionId])
  889. @@map("inspection_items")
  890. }
  891. model InspectionQuoteRequest {
  892. id String @id @default(cuid())
  893. status String @default("pending")
  894. message String?
  895. selectedItemIds String[]
  896. createdAt DateTime @default(now())
  897. inspectionId String
  898. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  899. organizationId String
  900. @@index([organizationId])
  901. @@index([inspectionId])
  902. @@map("inspection_quote_requests")
  903. }
  904. model Notification {
  905. id String @id @default(cuid())
  906. type String
  907. title String
  908. message String
  909. entityType String
  910. entityId String
  911. entityUrl String
  912. read Boolean @default(false)
  913. organizationId String
  914. createdAt DateTime @default(now())
  915. @@index([organizationId, createdAt(sort: Desc)])
  916. @@index([organizationId, read])
  917. @@map("notifications")
  918. }
  919. model SmsMessage {
  920. id String @id @default(cuid())
  921. direction String // "inbound" | "outbound"
  922. fromNumber String
  923. toNumber String
  924. body String
  925. status String @default("queued") // queued | sent | delivered | failed | received
  926. providerMsgId String?
  927. errorMessage String?
  928. relatedEntityType String?
  929. relatedEntityId String?
  930. createdAt DateTime @default(now())
  931. updatedAt DateTime @updatedAt
  932. organizationId String
  933. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  934. customerId String?
  935. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  936. @@index([organizationId, customerId, createdAt(sort: Desc)])
  937. @@index([organizationId, createdAt(sort: Desc)])
  938. @@index([toNumber, organizationId])
  939. @@map("sms_messages")
  940. }
  941. model TelegramMessage {
  942. id String @id @default(cuid())
  943. direction String // "inbound" | "outbound"
  944. chatId String // Telegram chat ID
  945. body String
  946. status String @default("queued") // queued | sent | delivered | failed | received
  947. telegramMessageId String? // Telegram's message ID for tracking
  948. errorMessage String?
  949. relatedEntityType String?
  950. relatedEntityId String?
  951. createdAt DateTime @default(now())
  952. updatedAt DateTime @updatedAt
  953. organizationId String
  954. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  955. customerId String?
  956. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  957. @@index([organizationId, customerId, createdAt(sort: Desc)])
  958. @@index([organizationId, chatId, createdAt(sort: Desc)])
  959. @@map("telegram_messages")
  960. }
  961. model Technician {
  962. id String @id @default(cuid())
  963. name String
  964. color String @default("#3b82f6")
  965. isActive Boolean @default(true)
  966. sortOrder Int @default(0)
  967. dailyCapacity Int @default(480)
  968. userId String?
  969. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  970. organizationId String
  971. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  972. serviceRecords ServiceRecord[]
  973. inspections Inspection[]
  974. statusReports StatusReport[]
  975. createdAt DateTime @default(now())
  976. updatedAt DateTime @updatedAt
  977. @@index([organizationId])
  978. @@map("technicians")
  979. }
  980. model AuditLog {
  981. id String @id @default(cuid())
  982. timestamp DateTime @default(now())
  983. action String
  984. entity String?
  985. entityId String?
  986. message String?
  987. metadata Json?
  988. ip String?
  989. userAgent String?
  990. userId String?
  991. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  992. organizationId String?
  993. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
  994. @@index([organizationId, timestamp(sort: Desc)])
  995. @@index([userId, timestamp(sort: Desc)])
  996. @@index([action, timestamp(sort: Desc)])
  997. @@map("audit_logs")
  998. }
  999. model CustomerSession {
  1000. id String @id @default(cuid())
  1001. token String @unique
  1002. expiresAt DateTime
  1003. createdAt DateTime @default(now())
  1004. customerId String
  1005. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1006. organizationId String
  1007. @@index([token])
  1008. @@index([customerId, organizationId])
  1009. @@map("customer_sessions")
  1010. }
  1011. model CustomerMagicLink {
  1012. id String @id @default(cuid())
  1013. token String @unique
  1014. email String
  1015. organizationId String
  1016. expiresAt DateTime
  1017. usedAt DateTime?
  1018. createdAt DateTime @default(now())
  1019. @@index([token])
  1020. @@map("customer_magic_links")
  1021. }
  1022. model CustomerSmsCode {
  1023. id String @id @default(cuid())
  1024. code String
  1025. phone String
  1026. organizationId String
  1027. expiresAt DateTime
  1028. usedAt DateTime?
  1029. createdAt DateTime @default(now())
  1030. @@index([phone, organizationId])
  1031. @@index([organizationId])
  1032. @@map("customer_sms_codes")
  1033. }
  1034. model ServiceRequest {
  1035. id String @id @default(cuid())
  1036. description String
  1037. preferredDate DateTime?
  1038. status String @default("pending")
  1039. adminNotes String?
  1040. createdAt DateTime @default(now())
  1041. updatedAt DateTime @updatedAt
  1042. customerId String
  1043. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1044. vehicleId String
  1045. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1046. organizationId String
  1047. @@index([organizationId, status])
  1048. @@index([customerId])
  1049. @@map("service_requests")
  1050. }
  1051. model AiGeneratedMessage {
  1052. id String @id @default(cuid())
  1053. vehicleId String
  1054. type String // "summary" | "common_issues" | etc.
  1055. content String
  1056. createdAt DateTime @default(now())
  1057. updatedAt DateTime @updatedAt
  1058. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1059. @@unique([vehicleId, type])
  1060. @@map("ai_generated_messages")
  1061. }
  1062. model AiChat {
  1063. id String @id @default(cuid())
  1064. title String @default("New chat")
  1065. createdAt DateTime @default(now())
  1066. updatedAt DateTime @updatedAt
  1067. userId String
  1068. organizationId String
  1069. messages AiChatMessage[]
  1070. @@index([organizationId, userId, updatedAt(sort: Desc)])
  1071. @@map("ai_chats")
  1072. }
  1073. model AiChatMessage {
  1074. id String @id @default(cuid())
  1075. role String // "user" | "assistant"
  1076. content String
  1077. createdAt DateTime @default(now())
  1078. chatId String
  1079. chat AiChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
  1080. @@index([chatId, createdAt])
  1081. @@map("ai_chat_messages")
  1082. }
  1083. model StatusReport {
  1084. id String @id @default(cuid())
  1085. title String?
  1086. message String? @db.Text
  1087. videoUrl String?
  1088. videoFileName String?
  1089. publicToken String @unique
  1090. status String @default("draft") // draft | published | sent | viewed
  1091. sentVia String? // email, sms, telegram (comma-separated)
  1092. sentAt DateTime?
  1093. viewedAt DateTime?
  1094. customerFeedback String? @db.Text
  1095. feedbackAt DateTime?
  1096. expiresAt DateTime?
  1097. createdAt DateTime @default(now())
  1098. updatedAt DateTime @updatedAt
  1099. serviceRecordId String
  1100. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  1101. organizationId String
  1102. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1103. technicianId String?
  1104. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  1105. @@index([serviceRecordId])
  1106. @@index([organizationId])
  1107. @@index([publicToken])
  1108. @@index([expiresAt])
  1109. @@map("status_reports")
  1110. }
  1111. model ReportSchedule {
  1112. id String @id @default(cuid())
  1113. name String @default("Scheduled Report")
  1114. frequency String // "daily" | "weekly" | "monthly" etc.
  1115. dateRange String @default("last30d") // "last1d" | "last7d" | "last30d" | "ytd" | "allTime" etc.
  1116. sections String // JSON array of section keys
  1117. recipients String // JSON array of user IDs
  1118. nextRunDate DateTime
  1119. endDate DateTime?
  1120. isActive Boolean @default(true)
  1121. lastRunAt DateTime?
  1122. runCount Int @default(0)
  1123. createdAt DateTime @default(now())
  1124. updatedAt DateTime @updatedAt
  1125. organizationId String
  1126. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1127. createdById String
  1128. @@index([organizationId])
  1129. @@index([nextRunDate, isActive])
  1130. @@map("report_schedules")
  1131. }
  1132. model Webhook {
  1133. id String @id @default(cuid())
  1134. name String
  1135. url String
  1136. secret String
  1137. events String // JSON array of event names; "*" matches all
  1138. isActive Boolean @default(true)
  1139. description String?
  1140. lastTriggeredAt DateTime?
  1141. lastSuccessAt DateTime?
  1142. lastFailureAt DateTime?
  1143. failureCount Int @default(0)
  1144. createdAt DateTime @default(now())
  1145. updatedAt DateTime @updatedAt
  1146. organizationId String
  1147. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1148. createdById String
  1149. deliveries WebhookDelivery[]
  1150. @@index([organizationId])
  1151. @@index([organizationId, isActive])
  1152. @@map("webhooks")
  1153. }
  1154. model WebhookDelivery {
  1155. id String @id @default(cuid())
  1156. event String
  1157. payload String // JSON-stringified event payload as sent
  1158. status String @default("pending") // pending | success | failed | retrying
  1159. statusCode Int?
  1160. responseBody String?
  1161. errorMessage String?
  1162. attempt Int @default(0)
  1163. maxAttempts Int @default(5)
  1164. nextRetryAt DateTime?
  1165. deliveredAt DateTime?
  1166. durationMs Int?
  1167. createdAt DateTime @default(now())
  1168. updatedAt DateTime @updatedAt
  1169. webhookId String
  1170. webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
  1171. @@index([webhookId, createdAt(sort: Desc)])
  1172. @@index([status, nextRetryAt])
  1173. @@map("webhook_deliveries")
  1174. }