vehicles.prisma 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. model Vehicle {
  2. id String @id @default(cuid())
  3. make String
  4. model String
  5. year Int
  6. vin String?
  7. licensePlate String?
  8. color String?
  9. mileage Int @default(0)
  10. fuelType String? @default("gasoline")
  11. transmission String? @default("automatic")
  12. engineSize String?
  13. engineCode String?
  14. /// German type approval key from the registration certificate: HSN is the
  15. /// four-digit manufacturer code in field 2.1, TSN the three-character type
  16. /// code at the start of field 2.2. Parts catalogues and insurers look a car
  17. /// up by the pair, since Germany has no public plate registry.
  18. hsn String?
  19. tsn String?
  20. purchaseDate DateTime?
  21. purchasePrice Float?
  22. imageUrl String?
  23. /// sedan | hatchback | estate | suv | van | pickup | motorcycle | boat: which
  24. /// line drawing the condition map is marked on. Null until somebody picks
  25. /// one, and the map then guesses from the workshop's trade.
  26. bodyType String?
  27. isArchived Boolean @default(false)
  28. archiveReason String?
  29. /// Set when the customer said in the portal that the vehicle is no longer theirs.
  30. soldReportedAt DateTime?
  31. maintenanceDismissed Boolean @default(false)
  32. maintenanceDismissedAt DateTime?
  33. aiMessages AiGeneratedMessage[]
  34. aiChats AiChat[]
  35. createdAt DateTime @default(now())
  36. updatedAt DateTime @updatedAt
  37. userId String
  38. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  39. organizationId String?
  40. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  41. customerId String?
  42. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  43. serviceRecords ServiceRecord[]
  44. notes Note[]
  45. fuelLogs FuelLog[]
  46. reminders Reminder[]
  47. quotes Quote[]
  48. recurringInvoices RecurringInvoice[]
  49. inspections Inspection[]
  50. serviceRequests ServiceRequest[]
  51. findings VehicleFinding[]
  52. scheduledMessages ScheduledMessage[]
  53. tireSets TireSet[]
  54. inspectionStatus VehicleInspectionStatus?
  55. inspectionReminders InspectionReminderSend[]
  56. conditionMarks ConditionMark[]
  57. importBatchId String?
  58. importBatch ImportBatch? @relation(fields: [importBatchId], references: [id], onDelete: SetNull)
  59. @@index([organizationId])
  60. @@index([importBatchId])
  61. @@map("vehicles")
  62. }
  63. model Note {
  64. id String @id @default(cuid())
  65. title String
  66. content String
  67. isPinned Boolean @default(false)
  68. createdAt DateTime @default(now())
  69. updatedAt DateTime @updatedAt
  70. vehicleId String
  71. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  72. @@index([vehicleId])
  73. @@map("notes")
  74. }
  75. model FuelLog {
  76. id String @id @default(cuid())
  77. date DateTime @default(now())
  78. mileage Int
  79. gallons Float
  80. pricePerGallon Float
  81. totalCost Float
  82. isFillUp Boolean @default(true)
  83. station String?
  84. notes String?
  85. createdAt DateTime @default(now())
  86. updatedAt DateTime @updatedAt
  87. vehicleId String
  88. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  89. @@index([vehicleId])
  90. @@map("fuel_logs")
  91. }
  92. model Reminder {
  93. id String @id @default(cuid())
  94. title String
  95. description String?
  96. dueDate DateTime?
  97. /// True when the time of day in dueDate was chosen, not the noon a
  98. /// day-only reminder is stored at
  99. hasDueTime Boolean @default(false)
  100. dueMileage Int?
  101. isCompleted Boolean @default(false)
  102. /// Per-reminder notification channels, chosen when creating the reminder
  103. notifyInApp Boolean @default(true)
  104. notifyEmail Boolean @default(false)
  105. /// Set by the due-reminder cron once notified; cleared when dueDate changes
  106. notifiedAt DateTime?
  107. createdAt DateTime @default(now())
  108. updatedAt DateTime @updatedAt
  109. /// A reminder relates to a vehicle, a customer, or just the workshop
  110. /// (both null). organizationId is the canonical scope for all three.
  111. vehicleId String?
  112. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  113. customerId String?
  114. customer Customer? @relation(fields: [customerId], references: [id], onDelete: Cascade)
  115. organizationId String?
  116. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  117. @@index([vehicleId])
  118. @@index([customerId])
  119. @@index([organizationId])
  120. @@map("reminders")
  121. }
  122. model VehicleFinding {
  123. id String @id @default(cuid())
  124. description String
  125. severity String @default("needs_work")
  126. status String @default("open")
  127. notes String?
  128. imageUrls String[] @default([])
  129. createdAt DateTime @default(now())
  130. updatedAt DateTime @updatedAt
  131. vehicleId String
  132. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  133. serviceRecordId String?
  134. serviceRecord ServiceRecord? @relation("FindingDiscoveredIn", fields: [serviceRecordId], references: [id], onDelete: SetNull)
  135. resolvedServiceRecordId String?
  136. resolvedServiceRecord ServiceRecord? @relation("FindingResolvedIn", fields: [resolvedServiceRecordId], references: [id], onDelete: SetNull)
  137. /// The customer concern this finding answers, if it answers one.
  138. ///
  139. /// Nullable because plenty of findings are things nobody asked about: the
  140. /// perished bush somebody noticed while the car was on the lift. Those are
  141. /// worth just as much, they just are not part of the story the customer
  142. /// started. SetNull rather than Cascade because a finding is a fact about
  143. /// the vehicle and outlives the concern that prompted the look.
  144. concernId String?
  145. concern ServiceConcern? @relation(fields: [concernId], references: [id], onDelete: SetNull)
  146. @@index([vehicleId])
  147. @@index([serviceRecordId])
  148. @@index([concernId])
  149. @@map("vehicle_findings")
  150. }
  151. /// What a vehicle registry, or the workshop itself, last said about when the
  152. /// vehicle's periodic roadworthiness inspection (EU-kontroll, MOT, APK, HU,
  153. /// contrôle technique, and so on) is due. One row per vehicle. The
  154. /// organisation is on the row so "what is due for this workshop" is one
  155. /// indexed query and the isolation is explicit rather than inherited.
  156. model VehicleInspectionStatus {
  157. id String @id @default(cuid())
  158. vehicleId String @unique
  159. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  160. organizationId String
  161. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  162. /// Next periodic inspection deadline, when known.
  163. dueAt DateTime?
  164. /// Last approved periodic inspection, when known.
  165. lastAt DateTime?
  166. /// Connector id ("vegvesen"), or "manual" / "inspection" for the workshop's own entries.
  167. source String
  168. /// When a registry last confirmed the row. Null for manual entries.
  169. checkedAt DateTime?
  170. /// False when the registry answered but had no such vehicle.
  171. found Boolean @default(true)
  172. /// Whether the registry lists the vehicle as currently registered, when it says.
  173. registered Boolean?
  174. /// Last lookup failure, cleared on success. Never a plate or VIN.
  175. lastError String?
  176. /// Whatever else the registry returned and the app has no column for yet:
  177. /// approved tyre sizes, weights, vehicle class, first registration.
  178. extras Json?
  179. createdAt DateTime @default(now())
  180. updatedAt DateTime @updatedAt
  181. @@index([organizationId, dueAt])
  182. @@index([organizationId, checkedAt])
  183. @@map("vehicle_inspection_statuses")
  184. }
  185. /// One "remind these customers" action, created only by a person confirming
  186. /// a reviewed list. Never created by a timer.
  187. model InspectionReminderCampaign {
  188. id String @id @default(cuid())
  189. organizationId String
  190. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  191. createdById String
  192. /// Client-generated, so a double submit creates one campaign, not two.
  193. idempotencyToken String @unique
  194. /// Deadline window the list was built from, in days.
  195. windowDays Int
  196. channel String
  197. subject String?
  198. body String
  199. /// "sent" once rows exist; "dry_run" in demo mode, where nothing goes out.
  200. status String @default("sent")
  201. recipientCount Int @default(0)
  202. createdAt DateTime @default(now())
  203. sends InspectionReminderSend[]
  204. @@index([organizationId, createdAt])
  205. @@map("inspection_reminder_campaigns")
  206. }
  207. /// One customer told once about one vehicle's deadline. The unique pair of
  208. /// vehicle and deadline is the guarantee that no deadline is ever reminded
  209. /// twice, whatever the code above it does.
  210. model InspectionReminderSend {
  211. id String @id @default(cuid())
  212. campaignId String
  213. campaign InspectionReminderCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
  214. organizationId String
  215. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  216. vehicleId String
  217. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  218. customerId String
  219. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  220. /// The deadline as it stood when the reminder went out.
  221. dueAt DateTime
  222. channel String
  223. recipient String
  224. /// Booking link secret; the link is /b/<token>.
  225. token String @unique
  226. expiresAt DateTime
  227. scheduledMessageId String? @unique
  228. scheduledMessage ScheduledMessage? @relation(fields: [scheduledMessageId], references: [id], onDelete: SetNull)
  229. /// Set when the customer booked from the link.
  230. bookedAt DateTime?
  231. bookedServiceRecordId String?
  232. bookedServiceRequestId String?
  233. cancelledAt DateTime?
  234. createdAt DateTime @default(now())
  235. @@unique([vehicleId, dueAt])
  236. @@index([organizationId, createdAt])
  237. @@index([campaignId])
  238. @@map("inspection_reminder_sends")
  239. }