service-records.prisma 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. model ServiceRecord {
  2. id String @id @default(cuid())
  3. title String
  4. description String?
  5. type String @default("maintenance")
  6. status String @default("completed")
  7. cost Float @default(0)
  8. mileage Int?
  9. serviceDate DateTime @default(now())
  10. startDateTime DateTime?
  11. endDateTime DateTime?
  12. /// When the customer was told the vehicle would be ready. Not the booking:
  13. /// `endDateTime` is when the work is planned to finish, this is the promise.
  14. promisedAt DateTime?
  15. shopName String?
  16. techName String?
  17. parts String?
  18. laborHours Float?
  19. diagnosticNotes String?
  20. invoiceNotes String?
  21. subtotal Float @default(0)
  22. taxRate Float @default(0)
  23. taxAmount Float @default(0)
  24. taxInclusive Boolean @default(false)
  25. /// The taxes this document was totalled with, when the workshop splits
  26. /// its tax into more than one (Québec GST and QST, Indian CGST and SGST).
  27. /// Null on single-rate documents. taxRate stays the combined rate and
  28. /// taxAmount the combined amount, so anything that does not itemise needs
  29. /// no change. Shape in src/lib/tax-components.ts; read leniently so an old
  30. /// row renders on new code.
  31. taxComponents Json?
  32. totalAmount Float @default(0)
  33. invoiceNumber String?
  34. invoiceDate DateTime?
  35. invoiceDueDate DateTime?
  36. discountType String?
  37. discountValue Float @default(0)
  38. discountAmount Float @default(0)
  39. manuallyPaid Boolean @default(false)
  40. // Warranty tracking
  41. /// What the workshop tells the customer about its own warranty on this job:
  42. /// "included", "not_included", or null when it says nothing. Only an included
  43. /// warranty carries months, mileage and an expiry; the terms are printed for
  44. /// either statement. See src/lib/warranty.ts.
  45. warrantyStatus String?
  46. warrantyMonths Int?
  47. warrantyMileage Int?
  48. warrantyExpiresAt DateTime?
  49. warrantyNotes String?
  50. /// Print the car's condition map on this job's invoice: the job's own
  51. /// answer from the drop-off tab, on or off, or null to follow the invoice
  52. /// design, which answers for every invoice.
  53. conditionMapOnInvoice Boolean?
  54. publicToken String? @unique
  55. sharedAt DateTime?
  56. viewCount Int @default(0)
  57. lastViewedAt DateTime?
  58. createdAt DateTime @default(now())
  59. updatedAt DateTime @updatedAt
  60. /// Who opened the job, when a person did. Null for work the system made
  61. /// (a customer's online booking, a recurring invoice, an import) and for
  62. /// everything older than this column.
  63. createdById String?
  64. createdBy User? @relation("ServiceRecordCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
  65. /// First time this invoice reached the customer, by email or share link.
  66. /// Distinct from sharedAt, which tracks only the link and is cleared when
  67. /// the link is revoked; sending is not undone by withdrawing the link.
  68. sentAt DateTime?
  69. /// The design this invoice prints with while it is still being written.
  70. /// Null follows the customer's design, then the workshop's default. SetNull:
  71. /// a deleted design sends its drafts back to the default rather than
  72. /// taking them with it.
  73. designId String?
  74. design DocumentDesign? @relation(fields: [designId], references: [id], onDelete: SetNull)
  75. /// When the invoice became the customer's document: first send, share or
  76. /// payment. From then on the sheet prints from the three snapshots below,
  77. /// never from live settings, so a changed address, logo, payment terms or
  78. /// title cannot rewrite paper already issued, and a preview and a re-send
  79. /// always show the same sheet. Only an owner's unlock followed by a send
  80. /// captures a fresh copy: the corrected one.
  81. issuedAt DateTime?
  82. /// The look at issue. Restrict: a snapshot referenced by an invoice is a
  83. /// record and cannot be removed.
  84. issuedDesignSnapshotId String?
  85. issuedDesignSnapshot DocumentDesignSnapshot? @relation(fields: [issuedDesignSnapshotId], references: [id], onDelete: Restrict)
  86. issuedLogoSnapshotId String?
  87. issuedLogoSnapshot DocumentAssetSnapshot? @relation("IssuedLogoSnapshot", fields: [issuedLogoSnapshotId], references: [id], onDelete: Restrict)
  88. /// The signature of whoever opened the job, as it was at issue. Null when
  89. /// they had none, or the invoice was issued before signatures existed.
  90. issuedSignatureSnapshotId String?
  91. issuedSignatureSnapshot DocumentAssetSnapshot? @relation("IssuedSignatureSnapshot", fields: [issuedSignatureSnapshotId], references: [id], onDelete: Restrict)
  92. /// Everything else the sheet printed that the record does not own: the
  93. /// workshop's details and payment settings, the customer, the vehicle, the
  94. /// technician's name, the findings and the custom fields. Shape in
  95. /// issuedInvoice.ts; read leniently so old snapshots render on new code.
  96. issuedData Json?
  97. /// Set when an owner or admin deliberately reopens a locked document.
  98. /// Nullable rather than a boolean so the audit trail can show when, and the
  99. /// screen can say the document was reopened rather than never locked.
  100. /// "online" when the customer booked it from a link, so the office can tell.
  101. bookingSource String?
  102. editUnlockedAt DateTime?
  103. editUnlockedById String?
  104. /// The stored set this job is about, when it came out of the tire hotel.
  105. /// Without it a tire-change work order says nothing about which tires, and
  106. /// the technician has to go and ask.
  107. tireSetId String?
  108. tireSet TireSet? @relation(fields: [tireSetId], references: [id], onDelete: SetNull)
  109. /// One of the workshop's own statuses, under the stage `status` names
  110. /// ("Ready for pickup" under completed). A label with a colour and a
  111. /// follow-up, never something the system decides by: whether a job is open,
  112. /// shows in the portal or stops a clock is read from `status` alone. Cleared
  113. /// whenever `status` moves, since it belongs to the stage it was chosen under.
  114. customStatusId String?
  115. customStatus WorkOrderStatus? @relation(fields: [customStatusId], references: [id], onDelete: SetNull)
  116. /// When that status was chosen, for "Ready for pickup since 14:02".
  117. customStatusSince DateTime?
  118. /// Null for parts-only / counter sales; such records use customerId
  119. /// (or neither, for anonymous walk-in sales)
  120. vehicleId String?
  121. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  122. /// Direct customer for records without a vehicle. Vehicle-linked records
  123. /// leave this null and resolve the customer through the vehicle, so
  124. /// reassigning a vehicle keeps invoices following the vehicle's customer.
  125. customerId String?
  126. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  127. /// Set when the job came straight from an inspection, so the inspection can
  128. /// show what was raised from it. SetNull: deleting the inspection must not
  129. /// take the job — and its invoice — with it.
  130. inspectionId String?
  131. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  132. /// Denormalized from the vehicle historically; now the canonical scope so
  133. /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
  134. organizationId String?
  135. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  136. technicianId String?
  137. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  138. /// Where in the shop the job is booked in: a lift, a ramp, an alignment
  139. /// stand. Independent of the technician, because a shop plans its bays and
  140. /// its people separately and the work board can lane by either.
  141. workBayId String?
  142. workBay WorkBay? @relation(fields: [workBayId], references: [id], onDelete: SetNull)
  143. sortOrder Int @default(0)
  144. partItems ServicePart[]
  145. laborItems ServiceLabor[]
  146. attachments ServiceAttachment[]
  147. payments Payment[]
  148. statusReports StatusReport[]
  149. stockMovements StockMovement[]
  150. timeEntries TimeEntry[]
  151. concerns ServiceConcern[]
  152. conditionMarks ConditionMark[]
  153. discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
  154. resolvedFindings VehicleFinding[] @relation("FindingResolvedIn")
  155. importBatchId String?
  156. importBatch ImportBatch? @relation(fields: [importBatchId], references: [id], onDelete: SetNull)
  157. @@index([vehicleId])
  158. @@index([technicianId])
  159. @@index([workBayId])
  160. @@index([designId])
  161. @@index([issuedDesignSnapshotId])
  162. @@index([issuedLogoSnapshotId])
  163. @@index([issuedSignatureSnapshotId])
  164. @@index([organizationId])
  165. @@index([customerId])
  166. @@index([inspectionId])
  167. @@index([importBatchId])
  168. @@index([customStatusId])
  169. @@map("service_records")
  170. }
  171. /// One thing the customer asked about, in their words.
  172. ///
  173. /// A job is a list of these rather than a single note, because that is how the
  174. /// work actually divides: "it pulls right when braking" and "the aircon smells"
  175. /// are two conversations, two diagnoses and two lines on the bill. Storing them
  176. /// as one blob means the shop can answer one and quietly drop the other, which
  177. /// is the single most common reason a car comes back.
  178. ///
  179. /// This is the first of the trade's three Cs. The cause is a VehicleFinding
  180. /// pointing back here, carrying its own notes and photographs; the correction
  181. /// is the labour and parts on the job. Together they let the invoice explain
  182. /// itself: you said this, we found that, we did this about it.
  183. model ServiceConcern {
  184. id String @id @default(cuid())
  185. /// The customer's own words, kept as they said them.
  186. description String
  187. /// The order the customer raised them in, which is the order they care about.
  188. sortOrder Int @default(0)
  189. createdAt DateTime @default(now())
  190. updatedAt DateTime @updatedAt
  191. /// The rest of the story this concern starts: condition, cause, correction,
  192. /// confirmation. `description` is the condition. The cause is the sentence
  193. /// the customer is told; the structured findings hang off `findings` below.
  194. cause String?
  195. /// What was done about it.
  196. correction String?
  197. /// How the fix was checked: the test drive, the re-scan, the symptom gone.
  198. confirmation String?
  199. /// Set when somebody ticks the concern as confirmed, cleared when unticked.
  200. /// Stamped by the server from the session, never taken from the client.
  201. confirmedAt DateTime?
  202. confirmedById String?
  203. confirmedBy User? @relation("ConcernConfirmedBy", fields: [confirmedById], references: [id], onDelete: SetNull)
  204. serviceRecordId String
  205. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  206. /// What the shop found when it went looking. Empty means nobody has answered
  207. /// this one yet, which is worth being able to ask about before closing a job.
  208. findings VehicleFinding[]
  209. /// Photos and video filed under this concern.
  210. attachments ServiceAttachment[]
  211. @@index([serviceRecordId])
  212. @@map("service_concerns")
  213. }
  214. model Payment {
  215. id String @id @default(cuid())
  216. amount Float
  217. date DateTime @default(now())
  218. method String @default("other")
  219. note String?
  220. provider String?
  221. externalId String?
  222. createdAt DateTime @default(now())
  223. updatedAt DateTime @updatedAt
  224. serviceRecordId String
  225. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  226. /// A payment a vendor or a ledger reported is booked once per invoice. The
  227. /// browser coming back and the vendor's notification report the same payment
  228. /// in the same second, and looking before writing let both write. Rows with
  229. /// no provider or no external id, which is every payment typed in by hand,
  230. /// are not held to it: Postgres counts nulls as distinct.
  231. @@unique([serviceRecordId, provider, externalId])
  232. @@index([serviceRecordId])
  233. @@map("payments")
  234. }
  235. model ServiceAttachment {
  236. id String @id @default(cuid())
  237. fileName String
  238. fileUrl String
  239. fileType String
  240. fileSize Int
  241. category String @default("diagnostic")
  242. description String?
  243. includeInInvoice Boolean @default(true)
  244. createdAt DateTime @default(now())
  245. serviceRecordId String
  246. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  247. /// The concern this photo or video is evidence for, when it was added from
  248. /// that concern's row. It is still one of the job's files, listed with the
  249. /// rest. SetNull, because a picture of the car outlives the sentence it
  250. /// was filed under.
  251. concernId String?
  252. concern ServiceConcern? @relation(fields: [concernId], references: [id], onDelete: SetNull)
  253. @@index([serviceRecordId])
  254. @@index([concernId])
  255. @@map("service_attachments")
  256. }
  257. model ServicePart {
  258. id String @id @default(cuid())
  259. partNumber String?
  260. name String
  261. quantity Float @default(1)
  262. /// Unit of measure snapshotted from InventoryPart at pick time; null for
  263. /// free-text lines and legacy rows.
  264. unit String?
  265. unitPrice Float @default(0)
  266. total Float @default(0)
  267. unitCost Float @default(0)
  268. markupPercent Float @default(0)
  269. inventoryPartId String?
  270. serviceRecordId String
  271. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  272. @@index([serviceRecordId])
  273. @@map("service_parts")
  274. }
  275. model ServiceLabor {
  276. id String @id @default(cuid())
  277. description String
  278. hours Float @default(0)
  279. rate Float @default(0)
  280. total Float @default(0)
  281. pricingType String @default("hourly")
  282. serviceRecordId String
  283. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  284. @@index([serviceRecordId])
  285. @@map("service_labor")
  286. }
  287. model RecurringInvoice {
  288. id String @id @default(cuid())
  289. title String
  290. description String?
  291. frequency String // "weekly" | "biweekly" | "monthly" | "quarterly" | "yearly"
  292. nextRunDate DateTime
  293. endDate DateTime?
  294. isActive Boolean @default(true)
  295. lastRunAt DateTime?
  296. runCount Int @default(0)
  297. // Template fields (copied to ServiceRecord on generation)
  298. type String @default("maintenance")
  299. cost Float @default(0)
  300. taxRate Float @default(0)
  301. taxInclusive Boolean @default(false)
  302. /// Copied onto each generated invoice, like taxRate. See ServiceRecord.
  303. taxComponents Json?
  304. invoiceNotes String?
  305. vehicleId String
  306. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  307. createdAt DateTime @default(now())
  308. updatedAt DateTime @updatedAt
  309. templateParts RecurringPart[]
  310. templateLabor RecurringLabor[]
  311. @@index([vehicleId])
  312. @@index([nextRunDate, isActive])
  313. @@map("recurring_invoices")
  314. }
  315. model RecurringPart {
  316. id String @id @default(cuid())
  317. name String
  318. partNumber String?
  319. quantity Float @default(1)
  320. unitPrice Float @default(0)
  321. recurringInvoiceId String
  322. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  323. @@map("recurring_parts")
  324. }
  325. model RecurringLabor {
  326. id String @id @default(cuid())
  327. description String
  328. hours Float @default(0)
  329. rate Float @default(0)
  330. pricingType String @default("hourly")
  331. recurringInvoiceId String
  332. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  333. @@map("recurring_labor")
  334. }
  335. /// A status the workshop defined for its work orders, filed under one of the
  336. /// three fixed stages. The stages are what the app reads; these are what the
  337. /// workshop calls the steps in between, with what should happen when a job
  338. /// reaches one.
  339. model WorkOrderStatus {
  340. id String @id @default(cuid())
  341. name String
  342. /// 'pending' | 'in-progress' | 'completed': the stage it sits under.
  343. stage String
  344. /// A colour name from the app's palette, not a hex value, so it follows the theme.
  345. color String @default("slate")
  346. sortOrder Int @default(0)
  347. /// Offer to tell the customer when a job reaches this status. Asked, never
  348. /// sent on its own.
  349. notifyCustomer Boolean @default(false)
  350. /// The message that offer starts from, with {tokens}. Null uses the app's own.
  351. messageTemplate String?
  352. /// Removed from the menus but kept, so jobs that carry it still say what it was.
  353. archivedAt DateTime?
  354. createdAt DateTime @default(now())
  355. updatedAt DateTime @updatedAt
  356. organizationId String
  357. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  358. serviceRecords ServiceRecord[]
  359. @@index([organizationId, stage, sortOrder])
  360. @@map("work_order_statuses")
  361. }