service-records.prisma 18 KB

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