service-records.prisma 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. shopName String?
  13. techName String?
  14. parts String?
  15. laborHours Float?
  16. diagnosticNotes String?
  17. invoiceNotes String?
  18. subtotal Float @default(0)
  19. taxRate Float @default(0)
  20. taxAmount Float @default(0)
  21. taxInclusive Boolean @default(false)
  22. /// The taxes this document was totalled with, when the workshop splits
  23. /// its tax into more than one (Québec GST and QST, Indian CGST and SGST).
  24. /// Null on single-rate documents. taxRate stays the combined rate and
  25. /// taxAmount the combined amount, so anything that does not itemise needs
  26. /// no change. Shape in src/lib/tax-components.ts; read leniently so an old
  27. /// row renders on new code.
  28. taxComponents Json?
  29. totalAmount Float @default(0)
  30. invoiceNumber String?
  31. invoiceDate DateTime?
  32. invoiceDueDate DateTime?
  33. discountType String?
  34. discountValue Float @default(0)
  35. discountAmount Float @default(0)
  36. manuallyPaid Boolean @default(false)
  37. // Warranty tracking
  38. warrantyMonths Int?
  39. warrantyMileage Int?
  40. warrantyExpiresAt DateTime?
  41. warrantyNotes String?
  42. publicToken String? @unique
  43. sharedAt DateTime?
  44. viewCount Int @default(0)
  45. lastViewedAt DateTime?
  46. createdAt DateTime @default(now())
  47. updatedAt DateTime @updatedAt
  48. /// First time this invoice reached the customer, by email or share link.
  49. /// Distinct from sharedAt, which tracks only the link and is cleared when
  50. /// the link is revoked; sending is not undone by withdrawing the link.
  51. sentAt DateTime?
  52. /// The design this invoice prints with while it is still being written.
  53. /// Null follows the customer's design, then the workshop's default. SetNull:
  54. /// a deleted design sends its drafts back to the default rather than
  55. /// taking them with it.
  56. designId String?
  57. design DocumentDesign? @relation(fields: [designId], references: [id], onDelete: SetNull)
  58. /// When the invoice became the customer's document: first send, share or
  59. /// payment. From then on the sheet prints from the three snapshots below,
  60. /// never from live settings, so a changed address, logo, payment terms or
  61. /// title cannot rewrite paper already issued, and a preview and a re-send
  62. /// always show the same sheet. Only an owner's unlock followed by a send
  63. /// captures a fresh copy: the corrected one.
  64. issuedAt DateTime?
  65. /// The look at issue. Restrict: a snapshot referenced by an invoice is a
  66. /// record and cannot be removed.
  67. issuedDesignSnapshotId String?
  68. issuedDesignSnapshot DocumentDesignSnapshot? @relation(fields: [issuedDesignSnapshotId], references: [id], onDelete: Restrict)
  69. issuedLogoSnapshotId String?
  70. issuedLogoSnapshot DocumentAssetSnapshot? @relation(fields: [issuedLogoSnapshotId], references: [id], onDelete: Restrict)
  71. /// Everything else the sheet printed that the record does not own: the
  72. /// workshop's details and payment settings, the customer, the vehicle, the
  73. /// technician's name, the findings and the custom fields. Shape in
  74. /// issuedInvoice.ts; read leniently so old snapshots render on new code.
  75. issuedData Json?
  76. /// Set when an owner or admin deliberately reopens a locked document.
  77. /// Nullable rather than a boolean so the audit trail can show when, and the
  78. /// screen can say the document was reopened rather than never locked.
  79. /// "online" when the customer booked it from a link, so the office can tell.
  80. bookingSource String?
  81. editUnlockedAt DateTime?
  82. editUnlockedById String?
  83. /// The stored set this job is about, when it came out of the tire hotel.
  84. /// Without it a tire-change work order says nothing about which tires, and
  85. /// the technician has to go and ask.
  86. tireSetId String?
  87. tireSet TireSet? @relation(fields: [tireSetId], references: [id], onDelete: SetNull)
  88. /// Null for parts-only / counter sales; such records use customerId
  89. /// (or neither, for anonymous walk-in sales)
  90. vehicleId String?
  91. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  92. /// Direct customer for records without a vehicle. Vehicle-linked records
  93. /// leave this null and resolve the customer through the vehicle, so
  94. /// reassigning a vehicle keeps invoices following the vehicle's customer.
  95. customerId String?
  96. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  97. /// Set when the job came straight from an inspection, so the inspection can
  98. /// show what was raised from it. SetNull: deleting the inspection must not
  99. /// take the job — and its invoice — with it.
  100. inspectionId String?
  101. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  102. /// Denormalized from the vehicle historically; now the canonical scope so
  103. /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
  104. organizationId String?
  105. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  106. technicianId String?
  107. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  108. /// Where in the shop the job is booked in: a lift, a ramp, an alignment
  109. /// stand. Independent of the technician, because a shop plans its bays and
  110. /// its people separately and the work board can lane by either.
  111. workBayId String?
  112. workBay WorkBay? @relation(fields: [workBayId], references: [id], onDelete: SetNull)
  113. sortOrder Int @default(0)
  114. partItems ServicePart[]
  115. laborItems ServiceLabor[]
  116. attachments ServiceAttachment[]
  117. payments Payment[]
  118. statusReports StatusReport[]
  119. stockMovements StockMovement[]
  120. timeEntries TimeEntry[]
  121. concerns ServiceConcern[]
  122. discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
  123. resolvedFindings VehicleFinding[] @relation("FindingResolvedIn")
  124. importBatchId String?
  125. importBatch ImportBatch? @relation(fields: [importBatchId], references: [id], onDelete: SetNull)
  126. @@index([vehicleId])
  127. @@index([technicianId])
  128. @@index([workBayId])
  129. @@index([designId])
  130. @@index([issuedDesignSnapshotId])
  131. @@index([issuedLogoSnapshotId])
  132. @@index([organizationId])
  133. @@index([customerId])
  134. @@index([inspectionId])
  135. @@index([importBatchId])
  136. @@map("service_records")
  137. }
  138. /// One thing the customer asked about, in their words.
  139. ///
  140. /// A job is a list of these rather than a single note, because that is how the
  141. /// work actually divides: "it pulls right when braking" and "the aircon smells"
  142. /// are two conversations, two diagnoses and two lines on the bill. Storing them
  143. /// as one blob means the shop can answer one and quietly drop the other, which
  144. /// is the single most common reason a car comes back.
  145. ///
  146. /// This is the first of the trade's three Cs. The cause is a VehicleFinding
  147. /// pointing back here, carrying its own notes and photographs; the correction
  148. /// is the labour and parts on the job. Together they let the invoice explain
  149. /// itself: you said this, we found that, we did this about it.
  150. model ServiceConcern {
  151. id String @id @default(cuid())
  152. /// The customer's own words, kept as they said them.
  153. description String
  154. /// The order the customer raised them in, which is the order they care about.
  155. sortOrder Int @default(0)
  156. createdAt DateTime @default(now())
  157. updatedAt DateTime @updatedAt
  158. serviceRecordId String
  159. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  160. /// What the shop found when it went looking. Empty means nobody has answered
  161. /// this one yet, which is worth being able to ask about before closing a job.
  162. findings VehicleFinding[]
  163. @@index([serviceRecordId])
  164. @@map("service_concerns")
  165. }
  166. model Payment {
  167. id String @id @default(cuid())
  168. amount Float
  169. date DateTime @default(now())
  170. method String @default("other")
  171. note String?
  172. provider String?
  173. externalId String?
  174. createdAt DateTime @default(now())
  175. updatedAt DateTime @updatedAt
  176. serviceRecordId String
  177. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  178. /// A payment a vendor or a ledger reported is booked once per invoice. The
  179. /// browser coming back and the vendor's notification report the same payment
  180. /// in the same second, and looking before writing let both write. Rows with
  181. /// no provider or no external id, which is every payment typed in by hand,
  182. /// are not held to it: Postgres counts nulls as distinct.
  183. @@unique([serviceRecordId, provider, externalId])
  184. @@index([serviceRecordId])
  185. @@map("payments")
  186. }
  187. model ServiceAttachment {
  188. id String @id @default(cuid())
  189. fileName String
  190. fileUrl String
  191. fileType String
  192. fileSize Int
  193. category String @default("diagnostic")
  194. description String?
  195. includeInInvoice Boolean @default(true)
  196. createdAt DateTime @default(now())
  197. serviceRecordId String
  198. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  199. @@index([serviceRecordId])
  200. @@map("service_attachments")
  201. }
  202. model ServicePart {
  203. id String @id @default(cuid())
  204. partNumber String?
  205. name String
  206. quantity Float @default(1)
  207. /// Unit of measure snapshotted from InventoryPart at pick time; null for
  208. /// free-text lines and legacy rows.
  209. unit String?
  210. unitPrice Float @default(0)
  211. total Float @default(0)
  212. unitCost Float @default(0)
  213. markupPercent Float @default(0)
  214. inventoryPartId String?
  215. serviceRecordId String
  216. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  217. @@index([serviceRecordId])
  218. @@map("service_parts")
  219. }
  220. model ServiceLabor {
  221. id String @id @default(cuid())
  222. description String
  223. hours Float @default(0)
  224. rate Float @default(0)
  225. total Float @default(0)
  226. pricingType String @default("hourly")
  227. serviceRecordId String
  228. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  229. @@index([serviceRecordId])
  230. @@map("service_labor")
  231. }
  232. model RecurringInvoice {
  233. id String @id @default(cuid())
  234. title String
  235. description String?
  236. frequency String // "weekly" | "biweekly" | "monthly" | "quarterly" | "yearly"
  237. nextRunDate DateTime
  238. endDate DateTime?
  239. isActive Boolean @default(true)
  240. lastRunAt DateTime?
  241. runCount Int @default(0)
  242. // Template fields (copied to ServiceRecord on generation)
  243. type String @default("maintenance")
  244. cost Float @default(0)
  245. taxRate Float @default(0)
  246. taxInclusive Boolean @default(false)
  247. /// Copied onto each generated invoice, like taxRate. See ServiceRecord.
  248. taxComponents Json?
  249. invoiceNotes String?
  250. vehicleId String
  251. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  252. createdAt DateTime @default(now())
  253. updatedAt DateTime @updatedAt
  254. templateParts RecurringPart[]
  255. templateLabor RecurringLabor[]
  256. @@index([vehicleId])
  257. @@index([nextRunDate, isActive])
  258. @@map("recurring_invoices")
  259. }
  260. model RecurringPart {
  261. id String @id @default(cuid())
  262. name String
  263. partNumber String?
  264. quantity Float @default(1)
  265. unitPrice Float @default(0)
  266. recurringInvoiceId String
  267. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  268. @@map("recurring_parts")
  269. }
  270. model RecurringLabor {
  271. id String @id @default(cuid())
  272. description String
  273. hours Float @default(0)
  274. rate Float @default(0)
  275. pricingType String @default("hourly")
  276. recurringInvoiceId String
  277. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  278. @@map("recurring_labor")
  279. }