schema.prisma 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898
  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. tireWarehouses TireWarehouse[]
  43. tireSets TireSet[]
  44. tireSetAttachments TireSetAttachment[]
  45. tireMeasurements TireMeasurement[]
  46. tireMovements TireMovement[]
  47. tireTreatments TireTreatment[]
  48. @@map("users")
  49. }
  50. model AppSetting {
  51. id String @id @default(cuid())
  52. key String
  53. value String
  54. userId String
  55. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  56. organizationId String?
  57. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  58. @@unique([organizationId, key])
  59. @@index([organizationId])
  60. @@map("app_settings")
  61. }
  62. model Session {
  63. id String @id @default(cuid())
  64. expiresAt DateTime
  65. token String @unique
  66. createdAt DateTime @default(now())
  67. updatedAt DateTime @updatedAt
  68. ipAddress String?
  69. userAgent String?
  70. userId String
  71. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  72. @@index([userId])
  73. @@map("sessions")
  74. }
  75. model Account {
  76. id String @id @default(cuid())
  77. accountId String
  78. providerId String
  79. userId String
  80. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  81. accessToken String?
  82. refreshToken String?
  83. idToken String?
  84. accessTokenExpiresAt DateTime?
  85. refreshTokenExpiresAt DateTime?
  86. scope String?
  87. password String?
  88. createdAt DateTime @default(now())
  89. updatedAt DateTime @updatedAt
  90. @@index([userId])
  91. @@map("accounts")
  92. }
  93. model Verification {
  94. id String @id @default(cuid())
  95. identifier String @unique
  96. value String
  97. expiresAt DateTime
  98. createdAt DateTime? @default(now())
  99. updatedAt DateTime? @updatedAt
  100. @@map("verifications")
  101. }
  102. model TwoFactor {
  103. id String @id @default(cuid())
  104. secret String
  105. backupCodes String
  106. userId String @unique
  107. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  108. @@map("two_factor")
  109. }
  110. model Passkey {
  111. id String @id @default(cuid())
  112. name String?
  113. publicKey String
  114. userId String
  115. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  116. credentialID String @unique
  117. counter Int
  118. deviceType String
  119. backedUp Boolean
  120. transports String?
  121. aaguid String?
  122. createdAt DateTime @default(now())
  123. @@index([userId])
  124. @@map("passkeys")
  125. }
  126. model Vehicle {
  127. id String @id @default(cuid())
  128. make String
  129. model String
  130. year Int
  131. vin String?
  132. licensePlate String?
  133. color String?
  134. mileage Int @default(0)
  135. fuelType String? @default("gasoline")
  136. transmission String? @default("automatic")
  137. engineSize String?
  138. engineCode String?
  139. purchaseDate DateTime?
  140. purchasePrice Float?
  141. imageUrl String?
  142. isArchived Boolean @default(false)
  143. archiveReason String?
  144. maintenanceDismissed Boolean @default(false)
  145. maintenanceDismissedAt DateTime?
  146. aiMessages AiGeneratedMessage[]
  147. createdAt DateTime @default(now())
  148. updatedAt DateTime @updatedAt
  149. userId String
  150. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  151. organizationId String?
  152. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  153. customerId String?
  154. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  155. serviceRecords ServiceRecord[]
  156. notes Note[]
  157. fuelLogs FuelLog[]
  158. reminders Reminder[]
  159. quotes Quote[]
  160. recurringInvoices RecurringInvoice[]
  161. inspections Inspection[]
  162. serviceRequests ServiceRequest[]
  163. findings VehicleFinding[]
  164. scheduledMessages ScheduledMessage[]
  165. tireSets TireSet[]
  166. @@index([organizationId])
  167. @@map("vehicles")
  168. }
  169. model ServiceRecord {
  170. id String @id @default(cuid())
  171. title String
  172. description String?
  173. type String @default("maintenance")
  174. status String @default("completed")
  175. cost Float @default(0)
  176. mileage Int?
  177. serviceDate DateTime @default(now())
  178. startDateTime DateTime?
  179. endDateTime DateTime?
  180. shopName String?
  181. techName String?
  182. parts String?
  183. laborHours Float?
  184. diagnosticNotes String?
  185. invoiceNotes String?
  186. subtotal Float @default(0)
  187. taxRate Float @default(0)
  188. taxAmount Float @default(0)
  189. taxInclusive Boolean @default(false)
  190. totalAmount Float @default(0)
  191. invoiceNumber String?
  192. invoiceDate DateTime?
  193. invoiceDueDate DateTime?
  194. discountType String?
  195. discountValue Float @default(0)
  196. discountAmount Float @default(0)
  197. manuallyPaid Boolean @default(false)
  198. // Warranty tracking
  199. warrantyMonths Int?
  200. warrantyMileage Int?
  201. warrantyExpiresAt DateTime?
  202. warrantyNotes String?
  203. publicToken String? @unique
  204. sharedAt DateTime?
  205. viewCount Int @default(0)
  206. lastViewedAt DateTime?
  207. createdAt DateTime @default(now())
  208. updatedAt DateTime @updatedAt
  209. /// The stored set this job is about, when it came out of the tire hotel.
  210. /// Without it a tire-change work order says nothing about which tires, and
  211. /// the technician has to go and ask.
  212. tireSetId String?
  213. tireSet TireSet? @relation(fields: [tireSetId], references: [id], onDelete: SetNull)
  214. /// Null for parts-only / counter sales; such records use customerId
  215. /// (or neither, for anonymous walk-in sales)
  216. vehicleId String?
  217. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  218. /// Direct customer for records without a vehicle. Vehicle-linked records
  219. /// leave this null and resolve the customer through the vehicle, so
  220. /// reassigning a vehicle keeps invoices following the vehicle's customer.
  221. customerId String?
  222. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  223. /// Set when the job came straight from an inspection, so the inspection can
  224. /// show what was raised from it. SetNull: deleting the inspection must not
  225. /// take the job — and its invoice — with it.
  226. inspectionId String?
  227. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  228. /// Denormalized from the vehicle historically; now the canonical scope so
  229. /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
  230. organizationId String?
  231. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  232. technicianId String?
  233. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  234. sortOrder Int @default(0)
  235. partItems ServicePart[]
  236. laborItems ServiceLabor[]
  237. attachments ServiceAttachment[]
  238. payments Payment[]
  239. statusReports StatusReport[]
  240. stockMovements StockMovement[]
  241. discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
  242. resolvedFindings VehicleFinding[] @relation("FindingResolvedIn")
  243. @@index([vehicleId])
  244. @@index([technicianId])
  245. @@index([organizationId])
  246. @@index([customerId])
  247. @@index([inspectionId])
  248. @@map("service_records")
  249. }
  250. model Note {
  251. id String @id @default(cuid())
  252. title String
  253. content String
  254. isPinned Boolean @default(false)
  255. createdAt DateTime @default(now())
  256. updatedAt DateTime @updatedAt
  257. vehicleId String
  258. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  259. @@index([vehicleId])
  260. @@map("notes")
  261. }
  262. model FuelLog {
  263. id String @id @default(cuid())
  264. date DateTime @default(now())
  265. mileage Int
  266. gallons Float
  267. pricePerGallon Float
  268. totalCost Float
  269. isFillUp Boolean @default(true)
  270. station String?
  271. notes String?
  272. createdAt DateTime @default(now())
  273. updatedAt DateTime @updatedAt
  274. vehicleId String
  275. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  276. @@index([vehicleId])
  277. @@map("fuel_logs")
  278. }
  279. model Reminder {
  280. id String @id @default(cuid())
  281. title String
  282. description String?
  283. dueDate DateTime?
  284. dueMileage Int?
  285. isCompleted Boolean @default(false)
  286. /// Per-reminder notification channels, chosen when creating the reminder
  287. notifyInApp Boolean @default(true)
  288. notifyEmail Boolean @default(false)
  289. /// Set by the due-reminder cron once notified; cleared when dueDate changes
  290. notifiedAt DateTime?
  291. createdAt DateTime @default(now())
  292. updatedAt DateTime @updatedAt
  293. /// A reminder relates to a vehicle, a customer, or just the workshop
  294. /// (both null). organizationId is the canonical scope for all three.
  295. vehicleId String?
  296. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  297. customerId String?
  298. customer Customer? @relation(fields: [customerId], references: [id], onDelete: Cascade)
  299. organizationId String?
  300. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  301. @@index([vehicleId])
  302. @@index([customerId])
  303. @@index([organizationId])
  304. @@map("reminders")
  305. }
  306. model VehicleFinding {
  307. id String @id @default(cuid())
  308. description String
  309. severity String @default("needs_work")
  310. status String @default("open")
  311. notes String?
  312. imageUrls String[] @default([])
  313. createdAt DateTime @default(now())
  314. updatedAt DateTime @updatedAt
  315. vehicleId String
  316. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  317. serviceRecordId String?
  318. serviceRecord ServiceRecord? @relation("FindingDiscoveredIn", fields: [serviceRecordId], references: [id], onDelete: SetNull)
  319. resolvedServiceRecordId String?
  320. resolvedServiceRecord ServiceRecord? @relation("FindingResolvedIn", fields: [resolvedServiceRecordId], references: [id], onDelete: SetNull)
  321. @@index([vehicleId])
  322. @@index([serviceRecordId])
  323. @@map("vehicle_findings")
  324. }
  325. model Customer {
  326. id String @id @default(cuid())
  327. /// Human-facing customer number: auto-assigned sequentially on create,
  328. /// editable afterwards. Unique per organization (nulls allowed for
  329. /// legacy customers).
  330. customerNumber String?
  331. name String
  332. email String?
  333. phone String?
  334. address String?
  335. company String?
  336. taxId String?
  337. taxExempt Boolean @default(false)
  338. notes String?
  339. telegramChatId String?
  340. createdAt DateTime @default(now())
  341. updatedAt DateTime @updatedAt
  342. userId String
  343. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  344. organizationId String?
  345. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  346. vehicles Vehicle[]
  347. quotes Quote[]
  348. smsMessages SmsMessage[]
  349. telegramMessages TelegramMessage[]
  350. customerSessions CustomerSession[]
  351. scheduledMessages ScheduledMessage[]
  352. serviceRequests ServiceRequest[]
  353. serviceRecords ServiceRecord[]
  354. reminders Reminder[]
  355. tireSets TireSet[]
  356. @@unique([organizationId, customerNumber])
  357. @@index([organizationId])
  358. @@map("customers")
  359. }
  360. model Payment {
  361. id String @id @default(cuid())
  362. amount Float
  363. date DateTime @default(now())
  364. method String @default("other")
  365. note String?
  366. provider String?
  367. externalId String?
  368. createdAt DateTime @default(now())
  369. updatedAt DateTime @updatedAt
  370. serviceRecordId String
  371. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  372. @@index([serviceRecordId])
  373. @@map("payments")
  374. }
  375. model ServiceAttachment {
  376. id String @id @default(cuid())
  377. fileName String
  378. fileUrl String
  379. fileType String
  380. fileSize Int
  381. category String @default("diagnostic")
  382. description String?
  383. includeInInvoice Boolean @default(true)
  384. createdAt DateTime @default(now())
  385. serviceRecordId String
  386. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  387. @@index([serviceRecordId])
  388. @@map("service_attachments")
  389. }
  390. model ServicePart {
  391. id String @id @default(cuid())
  392. partNumber String?
  393. name String
  394. quantity Float @default(1)
  395. unitPrice Float @default(0)
  396. total Float @default(0)
  397. unitCost Float @default(0)
  398. markupPercent Float @default(0)
  399. inventoryPartId String?
  400. serviceRecordId String
  401. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  402. @@index([serviceRecordId])
  403. @@map("service_parts")
  404. }
  405. model ServiceLabor {
  406. id String @id @default(cuid())
  407. description String
  408. hours Float @default(0)
  409. rate Float @default(0)
  410. total Float @default(0)
  411. pricingType String @default("hourly")
  412. serviceRecordId String
  413. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  414. @@index([serviceRecordId])
  415. @@map("service_labor")
  416. }
  417. model LaborPreset {
  418. id String @id @default(cuid())
  419. name String
  420. description String?
  421. isArchived Boolean @default(false)
  422. createdAt DateTime @default(now())
  423. updatedAt DateTime @updatedAt
  424. userId String
  425. organizationId String
  426. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  427. items LaborPresetItem[]
  428. parts LaborPresetPart[]
  429. @@index([organizationId])
  430. @@map("labor_presets")
  431. }
  432. model LaborPresetItem {
  433. id String @id @default(cuid())
  434. description String
  435. hours Float @default(0)
  436. rate Float @default(0)
  437. pricingType String @default("hourly")
  438. sortOrder Int @default(0)
  439. presetId String
  440. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  441. @@index([presetId])
  442. @@map("labor_preset_items")
  443. }
  444. model LaborPresetPart {
  445. id String @id @default(cuid())
  446. name String
  447. partNumber String?
  448. quantity Float @default(1)
  449. unitPrice Float @default(0)
  450. inventoryPartId String?
  451. sortOrder Int @default(0)
  452. presetId String
  453. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  454. @@index([presetId])
  455. @@map("labor_preset_parts")
  456. }
  457. model InventoryPart {
  458. id String @id @default(cuid())
  459. partNumber String?
  460. barcode String?
  461. name String
  462. description String?
  463. category String?
  464. quantity Int @default(0)
  465. minQuantity Int @default(0)
  466. unitCost Float @default(0)
  467. sellPrice Float @default(0)
  468. supplier String?
  469. supplierPhone String?
  470. supplierEmail String?
  471. supplierUrl String?
  472. imageUrl String?
  473. location String?
  474. isArchived Boolean @default(false)
  475. /// Set when a low-stock alert has been sent for the current dip below
  476. /// `minQuantity`, and cleared once stock climbs back above it. This
  477. /// hysteresis is what stops a part that sits at zero from re-alerting on
  478. /// every check — it alerts once per descent, not once per run.
  479. lowStockAlertedAt DateTime?
  480. createdAt DateTime @default(now())
  481. updatedAt DateTime @updatedAt
  482. userId String
  483. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  484. organizationId String?
  485. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  486. gallery StoredImage[] @relation("InventoryPartImages")
  487. movements StockMovement[]
  488. @@unique([organizationId, barcode])
  489. @@index([organizationId])
  490. @@map("inventory_parts")
  491. }
  492. /// Append-only audit trail of every change to InventoryPart.quantity.
  493. ///
  494. /// Written inside the same transaction as the stock mutation itself, so the
  495. /// ledger can never drift from the balance it describes. `quantityAfter` is
  496. /// captured from the atomic `UPDATE ... RETURNING`, giving an exact running
  497. /// balance even under concurrent writes.
  498. model StockMovement {
  499. id String @id @default(cuid())
  500. /// Signed change in whole units. Negative = consumed, positive = restocked.
  501. delta Int
  502. /// Part balance immediately after this movement was applied.
  503. quantityAfter Int
  504. /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
  505. reason String
  506. note String?
  507. inventoryPartId String
  508. inventoryPart InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
  509. /// The job the part was used on. Kept on SetNull so history survives
  510. /// deletion of the service record — "where was this used" outlives the job.
  511. serviceRecordId String?
  512. serviceRecord ServiceRecord? @relation(fields: [serviceRecordId], references: [id], onDelete: SetNull)
  513. /// Denormalised so history still reads correctly after a service record is
  514. /// deleted and serviceRecordId goes null.
  515. serviceRecordLabel String?
  516. userId String?
  517. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  518. organizationId String
  519. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  520. createdAt DateTime @default(now())
  521. @@index([inventoryPartId, createdAt(sort: Desc)])
  522. @@index([organizationId, createdAt(sort: Desc)])
  523. @@index([serviceRecordId])
  524. @@map("stock_movements")
  525. }
  526. model StoredImage {
  527. id String @id @default(cuid())
  528. url String
  529. fileName String?
  530. description String?
  531. sortOrder Int @default(0)
  532. createdAt DateTime @default(now())
  533. inventoryPartId String?
  534. inventoryPart InventoryPart? @relation("InventoryPartImages", fields: [inventoryPartId], references: [id], onDelete: Cascade)
  535. /// Condition photos taken when a tire is measured. Second parent on this
  536. /// model — an image belongs to exactly one of them.
  537. tireMeasurementId String?
  538. tireMeasurement TireMeasurement? @relation("TireMeasurementImages", fields: [tireMeasurementId], references: [id], onDelete: Cascade)
  539. @@index([inventoryPartId])
  540. @@index([tireMeasurementId])
  541. @@map("stored_images")
  542. }
  543. model Quote {
  544. id String @id @default(cuid())
  545. quoteNumber String?
  546. title String
  547. description String?
  548. status String @default("draft")
  549. validUntil DateTime?
  550. subtotal Float @default(0)
  551. taxRate Float @default(0)
  552. taxAmount Float @default(0)
  553. taxInclusive Boolean @default(false)
  554. discountType String?
  555. discountValue Float @default(0)
  556. discountAmount Float @default(0)
  557. totalAmount Float @default(0)
  558. notes String?
  559. publicToken String?
  560. sharedAt DateTime?
  561. viewCount Int @default(0)
  562. lastViewedAt DateTime?
  563. customerMessage String?
  564. convertedToId String?
  565. createdAt DateTime @default(now())
  566. updatedAt DateTime @updatedAt
  567. userId String
  568. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  569. organizationId String?
  570. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  571. customerId String?
  572. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  573. /// The stored set this quote is about, when it came out of the tire hotel.
  574. /// SetNull rather than Cascade: a quote already sent is a document the
  575. /// customer holds, and deleting the set must not erase it.
  576. tireSetId String?
  577. tireSet TireSet? @relation(fields: [tireSetId], references: [id], onDelete: SetNull)
  578. vehicleId String?
  579. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  580. inspectionId String?
  581. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  582. partItems QuotePart[]
  583. laborItems QuoteLabor[]
  584. attachments QuoteAttachment[]
  585. @@index([organizationId])
  586. @@map("quotes")
  587. }
  588. model QuoteAttachment {
  589. id String @id @default(cuid())
  590. fileName String
  591. fileUrl String
  592. fileType String
  593. fileSize Int
  594. category String @default("image")
  595. description String?
  596. includeInInvoice Boolean @default(true)
  597. createdAt DateTime @default(now())
  598. quoteId String
  599. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  600. @@index([quoteId])
  601. @@map("quote_attachments")
  602. }
  603. model QuotePart {
  604. id String @id @default(cuid())
  605. partNumber String?
  606. name String
  607. quantity Float @default(1)
  608. /// Internal purchase cost — never shown to customers (quote PDF/share
  609. /// render unitPrice only)
  610. unitCost Float @default(0)
  611. markupPercent Float @default(0)
  612. unitPrice Float @default(0)
  613. total Float @default(0)
  614. excluded Boolean @default(false)
  615. /// Optional link to the stocked part this line was picked from. Carried into
  616. /// ServicePart on conversion so the job deducts the right inventory item.
  617. inventoryPartId String?
  618. quoteId String
  619. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  620. @@index([quoteId])
  621. @@map("quote_parts")
  622. }
  623. model QuoteLabor {
  624. id String @id @default(cuid())
  625. description String
  626. hours Float @default(0)
  627. rate Float @default(0)
  628. total Float @default(0)
  629. pricingType String @default("hourly")
  630. excluded Boolean @default(false)
  631. quoteId String
  632. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  633. @@index([quoteId])
  634. @@map("quote_labor")
  635. }
  636. model CustomFieldDefinition {
  637. id String @id @default(cuid())
  638. name String
  639. label String
  640. fieldType String @default("text")
  641. options String?
  642. required Boolean @default(false)
  643. entityType String
  644. sortOrder Int @default(0)
  645. isActive Boolean @default(true)
  646. createdAt DateTime @default(now())
  647. updatedAt DateTime @updatedAt
  648. userId String
  649. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  650. organizationId String?
  651. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  652. values CustomFieldValue[]
  653. @@unique([organizationId, name, entityType])
  654. @@index([organizationId])
  655. @@map("custom_field_definitions")
  656. }
  657. model CustomFieldValue {
  658. id String @id @default(cuid())
  659. value String
  660. entityId String
  661. entityType String
  662. fieldId String
  663. field CustomFieldDefinition @relation(fields: [fieldId], references: [id], onDelete: Cascade)
  664. @@unique([fieldId, entityId])
  665. @@index([fieldId])
  666. @@map("custom_field_values")
  667. }
  668. /// A user-defined dashboard card: a named, filtered table over one entity.
  669. /// The config shape (entity, filters, columns, limit) is validated against
  670. /// the field registry in customCardActions — nothing here reaches SQL raw.
  671. model DashboardWidget {
  672. id String @id @default(cuid())
  673. name String
  674. config Json
  675. userId String
  676. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  677. organizationId String
  678. createdAt DateTime @default(now())
  679. updatedAt DateTime @updatedAt
  680. @@index([userId, organizationId])
  681. @@map("dashboard_widgets")
  682. }
  683. model Organization {
  684. id String @id @default(cuid())
  685. name String
  686. portalSlug String? @unique
  687. createdAt DateTime @default(now())
  688. updatedAt DateTime @updatedAt
  689. members OrganizationMember[]
  690. roles Role[]
  691. vehicles Vehicle[]
  692. serviceRecords ServiceRecord[]
  693. reminders Reminder[]
  694. customers Customer[]
  695. quotes Quote[]
  696. inventoryParts InventoryPart[]
  697. laborPresets LaborPreset[]
  698. customFieldDefinitions CustomFieldDefinition[]
  699. settings AppSetting[]
  700. subscription Subscription?
  701. invitations TeamInvitation[]
  702. inspectionTemplates InspectionTemplate[]
  703. smsMessages SmsMessage[]
  704. telegramMessages TelegramMessage[]
  705. technicians Technician[]
  706. auditLogs AuditLog[]
  707. statusReports StatusReport[]
  708. reportSchedules ReportSchedule[]
  709. scheduledMessages ScheduledMessage[]
  710. webhooks Webhook[]
  711. stockMovements StockMovement[]
  712. tireWarehouses TireWarehouse[]
  713. tireLocations TireLocation[]
  714. tireSets TireSet[]
  715. tireSetAttachments TireSetAttachment[]
  716. tireMovements TireMovement[]
  717. tireTreatments TireTreatment[]
  718. @@map("organizations")
  719. }
  720. model OrganizationMember {
  721. id String @id @default(cuid())
  722. role String @default("member")
  723. userId String
  724. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  725. organizationId String
  726. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  727. roleId String?
  728. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  729. @@unique([userId, organizationId])
  730. @@map("organization_members")
  731. }
  732. model Role {
  733. id String @id @default(cuid())
  734. name String
  735. isAdmin Boolean @default(false)
  736. createdAt DateTime @default(now())
  737. updatedAt DateTime @updatedAt
  738. organizationId String
  739. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  740. permissions Permission[]
  741. members OrganizationMember[]
  742. teamInvitations TeamInvitation[]
  743. @@unique([organizationId, name])
  744. @@map("roles")
  745. }
  746. model Permission {
  747. id String @id @default(cuid())
  748. action String
  749. subject String
  750. roleId String
  751. role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
  752. @@unique([roleId, action, subject])
  753. @@index([roleId])
  754. @@map("permissions")
  755. }
  756. model SystemSetting {
  757. id String @id @default(cuid())
  758. key String @unique
  759. value String
  760. @@map("system_settings")
  761. }
  762. model TeamInvitation {
  763. id String @id @default(cuid())
  764. email String
  765. role String @default("member")
  766. token String @unique
  767. status String @default("pending") // pending | accepted | cancelled
  768. expiresAt DateTime
  769. createdAt DateTime @default(now())
  770. organizationId String
  771. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  772. invitedById String
  773. roleId String?
  774. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  775. @@unique([email, organizationId])
  776. @@index([token])
  777. @@map("team_invitations")
  778. }
  779. // Stripe-ready models (implementation in follow-up)
  780. model SubscriptionPlan {
  781. id String @id @default(cuid())
  782. name String
  783. stripePriceId String? @unique
  784. price Float @default(0)
  785. interval String @default("month") // "month" | "year"
  786. maxMembers Int @default(1)
  787. features String? // JSON feature flags
  788. isActive Boolean @default(true)
  789. createdAt DateTime @default(now())
  790. updatedAt DateTime @updatedAt
  791. subscriptions Subscription[]
  792. @@map("subscription_plans")
  793. }
  794. model Subscription {
  795. id String @id @default(cuid())
  796. status String @default("active") // active | canceled | past_due | trialing
  797. stripeSubscriptionId String? @unique
  798. stripeCustomerId String?
  799. currentPeriodStart DateTime?
  800. currentPeriodEnd DateTime?
  801. cancelAtPeriodEnd Boolean @default(false)
  802. createdAt DateTime @default(now())
  803. updatedAt DateTime @updatedAt
  804. organizationId String @unique
  805. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  806. planId String
  807. plan SubscriptionPlan @relation(fields: [planId], references: [id])
  808. @@map("subscriptions")
  809. }
  810. model RecurringInvoice {
  811. id String @id @default(cuid())
  812. title String
  813. description String?
  814. frequency String // "weekly" | "biweekly" | "monthly" | "quarterly" | "yearly"
  815. nextRunDate DateTime
  816. endDate DateTime?
  817. isActive Boolean @default(true)
  818. lastRunAt DateTime?
  819. runCount Int @default(0)
  820. // Template fields (copied to ServiceRecord on generation)
  821. type String @default("maintenance")
  822. cost Float @default(0)
  823. taxRate Float @default(0)
  824. taxInclusive Boolean @default(false)
  825. invoiceNotes String?
  826. vehicleId String
  827. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  828. createdAt DateTime @default(now())
  829. updatedAt DateTime @updatedAt
  830. templateParts RecurringPart[]
  831. templateLabor RecurringLabor[]
  832. @@index([vehicleId])
  833. @@index([nextRunDate, isActive])
  834. @@map("recurring_invoices")
  835. }
  836. model RecurringPart {
  837. id String @id @default(cuid())
  838. name String
  839. partNumber String?
  840. quantity Int @default(1)
  841. unitPrice Float @default(0)
  842. recurringInvoiceId String
  843. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  844. @@map("recurring_parts")
  845. }
  846. model RecurringLabor {
  847. id String @id @default(cuid())
  848. description String
  849. hours Float @default(0)
  850. rate Float @default(0)
  851. pricingType String @default("hourly")
  852. recurringInvoiceId String
  853. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  854. @@map("recurring_labor")
  855. }
  856. model InspectionTemplate {
  857. id String @id @default(cuid())
  858. name String
  859. description String?
  860. isDefault Boolean @default(false)
  861. createdAt DateTime @default(now())
  862. updatedAt DateTime @updatedAt
  863. // Regulatory profile. `country` is an ISO-3166 alpha-2 code (or null for a
  864. // shop-specific checklist), `standard` names the regime the template follows
  865. // (e.g. "eu-2014-45"), and `severityScale` picks which defect vocabulary the
  866. // technician grades against — see Lib/conditions.ts.
  867. country String?
  868. standard String? @default("custom")
  869. severityScale String @default("eu")
  870. /// Provenance, for templates that came from a package rather than being
  871. /// written here. `packageId` is namespaced and stable across renames
  872. /// ("torqvoice/eu-roadworthiness"), which is what lets the library tell an
  873. /// update from a duplicate. Null on a template the workshop wrote itself.
  874. packageId String?
  875. packageVersion String?
  876. /// Where it came from: "builtin", "file" or "catalogue".
  877. packageSource String?
  878. organizationId String
  879. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  880. sections InspectionTemplateSection[]
  881. inspections Inspection[]
  882. @@index([organizationId])
  883. @@index([organizationId, packageId])
  884. @@map("inspection_templates")
  885. }
  886. model InspectionTemplateSection {
  887. id String @id @default(cuid())
  888. name String
  889. description String?
  890. // Regulation reference for the section, e.g. "1" for Braking equipment in
  891. // Directive 2014/45/EU Annex I.
  892. code String?
  893. sortOrder Int @default(0)
  894. templateId String
  895. template InspectionTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
  896. items InspectionTemplateItem[]
  897. @@index([templateId])
  898. @@map("inspection_template_sections")
  899. }
  900. model InspectionTemplateItem {
  901. id String @id @default(cuid())
  902. name String
  903. description String?
  904. // Regulation reference for the check, e.g. "1.1.13".
  905. code String?
  906. sortOrder Int @default(0)
  907. // How the technician records the result: "condition" (defect grading only),
  908. // "measurement" (numeric value in `unit`, graded against min/max),
  909. // "text" (free text) or "choice" (one of `choices`).
  910. inputType String @default("condition")
  911. unit String?
  912. minValue Float?
  913. maxValue Float?
  914. choices String[] @default([])
  915. required Boolean @default(false)
  916. photoRequired Boolean @default(false)
  917. // Defect severity applied when a measurement falls outside min/max.
  918. defaultSeverity String?
  919. // Phrases this workshop wants offered when a defect is recorded against the
  920. // check, on top of the built-in catalogue. See Lib/defectCatalogue.ts.
  921. defectSuggestions String[] @default([])
  922. sectionId String
  923. section InspectionTemplateSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
  924. @@index([sectionId])
  925. @@map("inspection_template_items")
  926. }
  927. model Inspection {
  928. id String @id @default(cuid())
  929. status String @default("in_progress")
  930. mileage Int?
  931. notes String?
  932. startDateTime DateTime?
  933. endDateTime DateTime?
  934. publicToken String? @unique
  935. completedAt DateTime?
  936. createdAt DateTime @default(now())
  937. updatedAt DateTime @updatedAt
  938. vehicleId String
  939. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  940. templateId String
  941. template InspectionTemplate @relation(fields: [templateId], references: [id])
  942. technicianId String?
  943. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  944. sortOrder Int @default(0)
  945. // Roadworthiness certificate fields required by Directive 2014/45/EU Annex IV.
  946. // `inspectorName` and `certificateNumber` are snapshots taken at completion so
  947. // the issued certificate stays stable even if staff or settings change later.
  948. /// Snapshot of how this inspection was graded, taken when it was created.
  949. /// Read in preference to the template's current setting: a workshop that
  950. /// later switches a template to the EU scale must not relabel a certificate
  951. /// it already issued. Null on inspections that predate the snapshot; those
  952. /// fall back to the template, which the migration pinned to its old scale.
  953. severityScale String?
  954. country String?
  955. vehicleCategory String?
  956. nextTestDue DateTime?
  957. certificateNumber String?
  958. inspectorName String?
  959. testLocation String?
  960. organizationId String
  961. items InspectionItem[]
  962. quoteRequests InspectionQuoteRequest[]
  963. quotes Quote[]
  964. serviceRecords ServiceRecord[]
  965. @@index([vehicleId])
  966. @@index([organizationId])
  967. @@index([technicianId])
  968. @@map("inspections")
  969. }
  970. model InspectionItem {
  971. id String @id @default(cuid())
  972. name String
  973. section String
  974. sortOrder Int @default(0)
  975. condition String @default("not_inspected")
  976. notes String?
  977. imageUrls String[] @default([])
  978. // Copied from the template at creation so an inspection stays readable even
  979. // after its template is edited or deleted.
  980. description String?
  981. code String?
  982. sectionCode String?
  983. inputType String @default("condition")
  984. unit String?
  985. minValue Float?
  986. maxValue Float?
  987. choices String[] @default([])
  988. required Boolean @default(false)
  989. photoRequired Boolean @default(false)
  990. defaultSeverity String?
  991. defectSuggestions String[] @default([])
  992. measuredValue Float?
  993. textValue String?
  994. inspectionId String
  995. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  996. @@index([inspectionId])
  997. @@map("inspection_items")
  998. }
  999. model InspectionQuoteRequest {
  1000. id String @id @default(cuid())
  1001. status String @default("pending")
  1002. message String?
  1003. selectedItemIds String[]
  1004. createdAt DateTime @default(now())
  1005. inspectionId String
  1006. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  1007. organizationId String
  1008. @@index([organizationId])
  1009. @@index([inspectionId])
  1010. @@map("inspection_quote_requests")
  1011. }
  1012. model Notification {
  1013. id String @id @default(cuid())
  1014. type String
  1015. title String
  1016. message String
  1017. entityType String
  1018. entityId String
  1019. entityUrl String
  1020. read Boolean @default(false)
  1021. organizationId String
  1022. createdAt DateTime @default(now())
  1023. @@index([organizationId, createdAt(sort: Desc)])
  1024. @@index([organizationId, read])
  1025. @@map("notifications")
  1026. }
  1027. model SmsMessage {
  1028. id String @id @default(cuid())
  1029. direction String // "inbound" | "outbound"
  1030. fromNumber String
  1031. toNumber String
  1032. body String
  1033. status String @default("queued") // queued | sent | delivered | failed | received
  1034. providerMsgId String?
  1035. errorMessage String?
  1036. relatedEntityType String?
  1037. relatedEntityId String?
  1038. createdAt DateTime @default(now())
  1039. updatedAt DateTime @updatedAt
  1040. organizationId String
  1041. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1042. customerId String?
  1043. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1044. @@index([organizationId, customerId, createdAt(sort: Desc)])
  1045. @@index([organizationId, createdAt(sort: Desc)])
  1046. @@index([toNumber, organizationId])
  1047. @@map("sms_messages")
  1048. }
  1049. model TelegramMessage {
  1050. id String @id @default(cuid())
  1051. direction String // "inbound" | "outbound"
  1052. chatId String // Telegram chat ID
  1053. body String
  1054. status String @default("queued") // queued | sent | delivered | failed | received
  1055. telegramMessageId String? // Telegram's message ID for tracking
  1056. errorMessage String?
  1057. relatedEntityType String?
  1058. relatedEntityId String?
  1059. createdAt DateTime @default(now())
  1060. updatedAt DateTime @updatedAt
  1061. organizationId String
  1062. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1063. customerId String?
  1064. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1065. @@index([organizationId, customerId, createdAt(sort: Desc)])
  1066. @@index([organizationId, chatId, createdAt(sort: Desc)])
  1067. @@map("telegram_messages")
  1068. }
  1069. model Technician {
  1070. id String @id @default(cuid())
  1071. name String
  1072. color String @default("#3b82f6")
  1073. isActive Boolean @default(true)
  1074. sortOrder Int @default(0)
  1075. dailyCapacity Int @default(480)
  1076. userId String?
  1077. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  1078. organizationId String
  1079. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1080. serviceRecords ServiceRecord[]
  1081. inspections Inspection[]
  1082. statusReports StatusReport[]
  1083. createdAt DateTime @default(now())
  1084. updatedAt DateTime @updatedAt
  1085. @@index([organizationId])
  1086. @@map("technicians")
  1087. }
  1088. model AuditLog {
  1089. id String @id @default(cuid())
  1090. timestamp DateTime @default(now())
  1091. action String
  1092. entity String?
  1093. entityId String?
  1094. message String?
  1095. metadata Json?
  1096. ip String?
  1097. userAgent String?
  1098. userId String?
  1099. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  1100. organizationId String?
  1101. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
  1102. @@index([organizationId, timestamp(sort: Desc)])
  1103. @@index([userId, timestamp(sort: Desc)])
  1104. @@index([action, timestamp(sort: Desc)])
  1105. @@map("audit_logs")
  1106. }
  1107. model CustomerSession {
  1108. id String @id @default(cuid())
  1109. token String @unique
  1110. expiresAt DateTime
  1111. createdAt DateTime @default(now())
  1112. customerId String
  1113. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1114. organizationId String
  1115. @@index([token])
  1116. @@index([customerId, organizationId])
  1117. @@map("customer_sessions")
  1118. }
  1119. model CustomerMagicLink {
  1120. id String @id @default(cuid())
  1121. token String @unique
  1122. email String
  1123. organizationId String
  1124. expiresAt DateTime
  1125. usedAt DateTime?
  1126. createdAt DateTime @default(now())
  1127. @@index([token])
  1128. @@map("customer_magic_links")
  1129. }
  1130. model CustomerSmsCode {
  1131. id String @id @default(cuid())
  1132. code String
  1133. phone String
  1134. organizationId String
  1135. expiresAt DateTime
  1136. usedAt DateTime?
  1137. createdAt DateTime @default(now())
  1138. @@index([phone, organizationId])
  1139. @@index([organizationId])
  1140. @@map("customer_sms_codes")
  1141. }
  1142. model ServiceRequest {
  1143. id String @id @default(cuid())
  1144. description String
  1145. preferredDate DateTime?
  1146. status String @default("pending")
  1147. adminNotes String?
  1148. createdAt DateTime @default(now())
  1149. updatedAt DateTime @updatedAt
  1150. customerId String
  1151. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1152. vehicleId String
  1153. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1154. organizationId String
  1155. @@index([organizationId, status])
  1156. @@index([customerId])
  1157. @@map("service_requests")
  1158. }
  1159. model AiGeneratedMessage {
  1160. id String @id @default(cuid())
  1161. vehicleId String
  1162. type String // "summary" | "common_issues" | etc.
  1163. content String
  1164. createdAt DateTime @default(now())
  1165. updatedAt DateTime @updatedAt
  1166. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1167. @@unique([vehicleId, type])
  1168. @@map("ai_generated_messages")
  1169. }
  1170. model AiChat {
  1171. id String @id @default(cuid())
  1172. title String @default("New chat")
  1173. createdAt DateTime @default(now())
  1174. updatedAt DateTime @updatedAt
  1175. userId String
  1176. organizationId String
  1177. messages AiChatMessage[]
  1178. @@index([organizationId, userId, updatedAt(sort: Desc)])
  1179. @@map("ai_chats")
  1180. }
  1181. model AiChatMessage {
  1182. id String @id @default(cuid())
  1183. role String // "user" | "assistant"
  1184. content String
  1185. createdAt DateTime @default(now())
  1186. chatId String
  1187. chat AiChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
  1188. @@index([chatId, createdAt])
  1189. @@map("ai_chat_messages")
  1190. }
  1191. model StatusReport {
  1192. id String @id @default(cuid())
  1193. title String?
  1194. message String? @db.Text
  1195. videoUrl String?
  1196. videoFileName String?
  1197. publicToken String @unique
  1198. status String @default("draft") // draft | published | sent | viewed
  1199. sentVia String? // email, sms, telegram (comma-separated)
  1200. sentAt DateTime?
  1201. viewedAt DateTime?
  1202. customerFeedback String? @db.Text
  1203. feedbackAt DateTime?
  1204. expiresAt DateTime?
  1205. createdAt DateTime @default(now())
  1206. updatedAt DateTime @updatedAt
  1207. serviceRecordId String
  1208. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  1209. organizationId String
  1210. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1211. technicianId String?
  1212. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  1213. @@index([serviceRecordId])
  1214. @@index([organizationId])
  1215. @@index([publicToken])
  1216. @@index([expiresAt])
  1217. @@map("status_reports")
  1218. }
  1219. model ReportSchedule {
  1220. id String @id @default(cuid())
  1221. name String @default("Scheduled Report")
  1222. frequency String // "daily" | "weekly" | "monthly" etc.
  1223. dateRange String @default("last30d") // "last1d" | "last7d" | "last30d" | "ytd" | "allTime" etc.
  1224. sections String // JSON array of section keys
  1225. recipients String // JSON array of user IDs
  1226. nextRunDate DateTime
  1227. endDate DateTime?
  1228. isActive Boolean @default(true)
  1229. lastRunAt DateTime?
  1230. runCount Int @default(0)
  1231. createdAt DateTime @default(now())
  1232. updatedAt DateTime @updatedAt
  1233. organizationId String
  1234. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1235. createdById String
  1236. @@index([organizationId])
  1237. @@index([nextRunDate, isActive])
  1238. @@map("report_schedules")
  1239. }
  1240. /// A message the workshop lines up to go out on a given day and time, through
  1241. /// whichever channel suits the customer. One-off by default; a frequency other
  1242. /// than "once" reschedules itself after each send, the way ReportSchedule does.
  1243. model ScheduledMessage {
  1244. id String @id @default(cuid())
  1245. channel String // "email" | "sms" | "telegram" | "in_app"
  1246. subject String? // email only
  1247. body String
  1248. /// Where it goes when no customer is attached, or when the workshop typed an
  1249. /// address by hand. Falls back to the customer's email/phone/chat id.
  1250. recipient String?
  1251. status String @default("scheduled") // scheduled | sent | failed | cancelled
  1252. sendAt DateTime
  1253. frequency String @default("once") // once | daily | weekly | biweekly | monthly | yearly
  1254. endDate DateTime?
  1255. lastRunAt DateTime?
  1256. sentAt DateTime?
  1257. runCount Int @default(0)
  1258. errorMessage String?
  1259. createdAt DateTime @default(now())
  1260. updatedAt DateTime @updatedAt
  1261. organizationId String
  1262. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1263. customerId String?
  1264. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1265. vehicleId String?
  1266. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  1267. createdById String
  1268. @@index([organizationId, sendAt])
  1269. @@index([status, sendAt])
  1270. @@map("scheduled_messages")
  1271. }
  1272. model Webhook {
  1273. id String @id @default(cuid())
  1274. name String
  1275. url String
  1276. secret String
  1277. events String // JSON array of event names; "*" matches all
  1278. isActive Boolean @default(true)
  1279. description String?
  1280. lastTriggeredAt DateTime?
  1281. lastSuccessAt DateTime?
  1282. lastFailureAt DateTime?
  1283. failureCount Int @default(0)
  1284. createdAt DateTime @default(now())
  1285. updatedAt DateTime @updatedAt
  1286. organizationId String
  1287. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1288. createdById String
  1289. deliveries WebhookDelivery[]
  1290. @@index([organizationId])
  1291. @@index([organizationId, isActive])
  1292. @@map("webhooks")
  1293. }
  1294. model WebhookDelivery {
  1295. id String @id @default(cuid())
  1296. event String
  1297. payload String // JSON-stringified event payload as sent
  1298. status String @default("pending") // pending | success | failed | retrying
  1299. statusCode Int?
  1300. responseBody String?
  1301. errorMessage String?
  1302. attempt Int @default(0)
  1303. maxAttempts Int @default(5)
  1304. nextRetryAt DateTime?
  1305. deliveredAt DateTime?
  1306. durationMs Int?
  1307. createdAt DateTime @default(now())
  1308. updatedAt DateTime @updatedAt
  1309. webhookId String
  1310. webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
  1311. @@index([webhookId, createdAt(sort: Desc)])
  1312. @@index([status, nextRetryAt])
  1313. @@map("webhook_deliveries")
  1314. }
  1315. // ---------------------------------------------------------------------------
  1316. // Tire hotel
  1317. //
  1318. // Seasonal tire storage. Opt-in per organization via the `tireHotel.enabled`
  1319. // app setting — every model here stays empty and every route stays hidden
  1320. // until a workshop switches it on.
  1321. //
  1322. // Capacity is counted in individual tires rather than sets, because a shelf
  1323. // that holds eight 15" tires does not hold eight 21" tires, and customers
  1324. // store anything from a single spare to a five-wheel set. Counting the real
  1325. // unit is the only thing that stays true in every market.
  1326. // ---------------------------------------------------------------------------
  1327. /// A physical building or room holding tire storage. Most workshops have one;
  1328. /// multi-site operators have several.
  1329. model TireWarehouse {
  1330. id String @id @default(cuid())
  1331. name String
  1332. address String?
  1333. notes String?
  1334. /// Preselected when checking tires in. Exactly one per organization.
  1335. isDefault Boolean @default(false)
  1336. isArchived Boolean @default(false)
  1337. organizationId String
  1338. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1339. userId String
  1340. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  1341. locations TireLocation[]
  1342. createdAt DateTime @default(now())
  1343. updatedAt DateTime @updatedAt
  1344. @@index([organizationId])
  1345. @@map("tire_warehouses")
  1346. }
  1347. /// One addressable storage slot: a shelf, a rack level, a pallet, a floor
  1348. /// square. The workshop names its own geography, so `zone`/`rack`/`shelf` are
  1349. /// free text rather than a fixed hierarchy — "Room B / Rack 4 / Shelf 2" and
  1350. /// "Container 1 / -- / Pallet 7" both fit.
  1351. model TireLocation {
  1352. id String @id @default(cuid())
  1353. /// Short human code used on labels and when directing a technician,
  1354. /// e.g. "B-04-2". Unique within a warehouse.
  1355. code String
  1356. zone String?
  1357. rack String?
  1358. shelf String?
  1359. position String?
  1360. /// How many individual tires fit here. The shelf overview divides stored
  1361. /// tires by this to show how full the location is.
  1362. capacity Int @default(8)
  1363. notes String?
  1364. isArchived Boolean @default(false)
  1365. warehouseId String
  1366. warehouse TireWarehouse @relation(fields: [warehouseId], references: [id], onDelete: Cascade)
  1367. organizationId String
  1368. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1369. tireSets TireSet[]
  1370. movesFrom TireMovement[] @relation("TireMovementFrom")
  1371. movesTo TireMovement[] @relation("TireMovementTo")
  1372. createdAt DateTime @default(now())
  1373. updatedAt DateTime @updatedAt
  1374. @@unique([warehouseId, code])
  1375. @@index([organizationId])
  1376. @@index([warehouseId, isArchived])
  1377. @@map("tire_locations")
  1378. }
  1379. /// A group of tires stored together for one vehicle. Usually four, but the
  1380. /// count is explicit so a pair or a five-wheel set works the same way.
  1381. model TireSet {
  1382. id String @id @default(cuid())
  1383. /// Sequential per organization, shown to staff and printed on labels.
  1384. reference String?
  1385. /// summer | winter | all_season | other — see TIRE_SEASONS in
  1386. /// src/features/tire-hotel/Lib/tireConstants.ts
  1387. season String @default("summer")
  1388. /// Studded winter tires are legal in some markets and banned in others, so
  1389. /// this is a flag on the season rather than a season of its own.
  1390. studded Boolean @default(false)
  1391. brand String?
  1392. model String?
  1393. /// Free text so every regional notation fits: "225/45R17", "P225/45R17 94V",
  1394. /// "31x10.5R15".
  1395. size String?
  1396. /// DOT / TIN week-year code used to judge tire age.
  1397. dotCode String?
  1398. loadSpeedIndex String?
  1399. /// Mounted on rims, which changes both handling and what a shelf holds.
  1400. withRims Boolean @default(false)
  1401. rimType String?
  1402. /// Tire pressure monitoring sensors present — they need care on refit.
  1403. hasTpms Boolean @default(false)
  1404. /// Individual tires in this set. Drives every capacity calculation.
  1405. quantity Int @default(4)
  1406. /// stored | released | disposed — see TIRE_SET_STATUSES
  1407. status String @default("stored")
  1408. notes String?
  1409. /// Where it physically sits. Null once released to the customer.
  1410. locationId String?
  1411. location TireLocation? @relation(fields: [locationId], references: [id], onDelete: SetNull)
  1412. vehicleId String?
  1413. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  1414. customerId String?
  1415. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1416. organizationId String
  1417. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1418. userId String
  1419. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  1420. checkedInAt DateTime?
  1421. checkedOutAt DateTime?
  1422. measurements TireMeasurement[]
  1423. movements TireMovement[]
  1424. treatments TireTreatment[]
  1425. attachments TireSetAttachment[]
  1426. quotes Quote[]
  1427. serviceRecords ServiceRecord[]
  1428. createdAt DateTime @default(now())
  1429. updatedAt DateTime @updatedAt
  1430. @@unique([organizationId, reference])
  1431. @@index([organizationId, status])
  1432. @@index([locationId])
  1433. @@index([vehicleId])
  1434. @@index([customerId])
  1435. @@map("tire_sets")
  1436. }
  1437. /// Condition of one tire at one moment. Kept as history rather than current
  1438. /// state so wear between seasons is visible, and so a damage dispute has a
  1439. /// dated record behind it.
  1440. /// A photo or document held against a stored set: a picture of a kerbed rim,
  1441. /// a damage report, the customer's written agreement to store them.
  1442. ///
  1443. /// Deliberately the same shape as ServiceAttachment, because that is where
  1444. /// these end up when the set is billed. Copying a row between two models with
  1445. /// the same fields cannot lose anything in translation, and the invoice PDF
  1446. /// already knows how to render one.
  1447. model TireSetAttachment {
  1448. id String @id @default(cuid())
  1449. fileName String
  1450. fileUrl String
  1451. fileType String
  1452. fileSize Int
  1453. /// What the technician wants said about it, e.g. "kerbing on the near side".
  1454. /// Travels onto the invoice as the caption under the image.
  1455. description String?
  1456. /// Whether it should reach the customer's invoice when this set is billed.
  1457. /// On by default: a photo taken at check-in is usually taken to be shown.
  1458. includeInInvoice Boolean @default(true)
  1459. sortOrder Int @default(0)
  1460. tireSetId String
  1461. tireSet TireSet @relation(fields: [tireSetId], references: [id], onDelete: Cascade)
  1462. organizationId String
  1463. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1464. uploadedById String?
  1465. uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: SetNull)
  1466. createdAt DateTime @default(now())
  1467. @@index([tireSetId])
  1468. @@index([organizationId])
  1469. @@map("tire_set_attachments")
  1470. }
  1471. model TireMeasurement {
  1472. id String @id @default(cuid())
  1473. /// front_left | front_right | rear_left | rear_right | spare | unspecified
  1474. position String @default("unspecified")
  1475. /// Always stored in millimetres. The UI converts to 32nds of an inch for
  1476. /// workshops on imperial units, so one number means the same thing in
  1477. /// every market.
  1478. treadDepthMm Float?
  1479. /// Always stored in bar. Converted to psi/kPa for display.
  1480. pressureBar Float?
  1481. /// good | fair | replace — see TIRE_CONDITIONS
  1482. condition String @default("good")
  1483. damage String?
  1484. notes String?
  1485. tireSetId String
  1486. tireSet TireSet @relation(fields: [tireSetId], references: [id], onDelete: Cascade)
  1487. /// Groups the four readings taken during one check-in or inspection so the
  1488. /// history reads as events rather than loose numbers.
  1489. movementId String?
  1490. movement TireMovement? @relation(fields: [movementId], references: [id], onDelete: SetNull)
  1491. measuredById String?
  1492. measuredBy User? @relation(fields: [measuredById], references: [id], onDelete: SetNull)
  1493. images StoredImage[] @relation("TireMeasurementImages")
  1494. measuredAt DateTime @default(now())
  1495. @@index([tireSetId, measuredAt(sort: Desc)])
  1496. @@index([movementId])
  1497. @@map("tire_measurements")
  1498. }
  1499. /// Every physical handling of a set: arrival, departure, a move between
  1500. /// shelves, disposal. This is the storage audit trail — "where has this been
  1501. /// and who touched it" has to outlive the set's current row.
  1502. model TireMovement {
  1503. id String @id @default(cuid())
  1504. /// check_in | check_out | relocate | dispose — see TIRE_MOVEMENT_TYPES
  1505. type String
  1506. /// Denormalised location codes so history still reads correctly after a
  1507. /// shelf is renamed or deleted.
  1508. fromCode String?
  1509. toCode String?
  1510. fromLocationId String?
  1511. fromLocation TireLocation? @relation("TireMovementFrom", fields: [fromLocationId], references: [id], onDelete: SetNull)
  1512. toLocationId String?
  1513. toLocation TireLocation? @relation("TireMovementTo", fields: [toLocationId], references: [id], onDelete: SetNull)
  1514. note String?
  1515. tireSetId String
  1516. tireSet TireSet @relation(fields: [tireSetId], references: [id], onDelete: Cascade)
  1517. organizationId String
  1518. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1519. performedById String?
  1520. performedBy User? @relation(fields: [performedById], references: [id], onDelete: SetNull)
  1521. measurements TireMeasurement[]
  1522. createdAt DateTime @default(now())
  1523. @@index([tireSetId, createdAt(sort: Desc)])
  1524. @@index([organizationId, createdAt(sort: Desc)])
  1525. @@map("tire_movements")
  1526. }
  1527. /// Work a set needs before it goes back on the car, or before it goes on the
  1528. /// shelf: washing, balancing, a TPMS service.
  1529. ///
  1530. /// A task with a state rather than a flag on the set, because the tire
  1531. /// department's real question is "what is left to do on this one", and a
  1532. /// boolean cannot answer that once someone has started. Recording who
  1533. /// finished it also settles the "was this washed?" conversation at pickup.
  1534. model TireTreatment {
  1535. id String @id @default(cuid())
  1536. /// wash_tires | wash_rims | balance | tpms_service | new_valves | repair
  1537. /// — see TREATMENT_TYPES
  1538. type String
  1539. /// pending | done | skipped — see TREATMENT_STATUSES
  1540. status String @default("pending")
  1541. notes String?
  1542. tireSetId String
  1543. tireSet TireSet @relation(fields: [tireSetId], references: [id], onDelete: Cascade)
  1544. completedAt DateTime?
  1545. completedById String?
  1546. completedBy User? @relation(fields: [completedById], references: [id], onDelete: SetNull)
  1547. organizationId String
  1548. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1549. createdAt DateTime @default(now())
  1550. updatedAt DateTime @updatedAt
  1551. /// One row per kind of work per set: asking for a wash twice is the same
  1552. /// request, not two.
  1553. @@unique([tireSetId, type])
  1554. @@index([organizationId, status])
  1555. @@map("tire_treatments")
  1556. }