schema.prisma 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548
  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. @@map("users")
  43. }
  44. model AppSetting {
  45. id String @id @default(cuid())
  46. key String
  47. value String
  48. userId String
  49. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  50. organizationId String?
  51. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  52. @@unique([organizationId, key])
  53. @@index([organizationId])
  54. @@map("app_settings")
  55. }
  56. model Session {
  57. id String @id @default(cuid())
  58. expiresAt DateTime
  59. token String @unique
  60. createdAt DateTime @default(now())
  61. updatedAt DateTime @updatedAt
  62. ipAddress String?
  63. userAgent String?
  64. userId String
  65. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  66. @@index([userId])
  67. @@map("sessions")
  68. }
  69. model Account {
  70. id String @id @default(cuid())
  71. accountId String
  72. providerId String
  73. userId String
  74. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  75. accessToken String?
  76. refreshToken String?
  77. idToken String?
  78. accessTokenExpiresAt DateTime?
  79. refreshTokenExpiresAt DateTime?
  80. scope String?
  81. password String?
  82. createdAt DateTime @default(now())
  83. updatedAt DateTime @updatedAt
  84. @@index([userId])
  85. @@map("accounts")
  86. }
  87. model Verification {
  88. id String @id @default(cuid())
  89. identifier String @unique
  90. value String
  91. expiresAt DateTime
  92. createdAt DateTime? @default(now())
  93. updatedAt DateTime? @updatedAt
  94. @@map("verifications")
  95. }
  96. model TwoFactor {
  97. id String @id @default(cuid())
  98. secret String
  99. backupCodes String
  100. userId String @unique
  101. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  102. @@map("two_factor")
  103. }
  104. model Passkey {
  105. id String @id @default(cuid())
  106. name String?
  107. publicKey String
  108. userId String
  109. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  110. credentialID String @unique
  111. counter Int
  112. deviceType String
  113. backedUp Boolean
  114. transports String?
  115. aaguid String?
  116. createdAt DateTime @default(now())
  117. @@index([userId])
  118. @@map("passkeys")
  119. }
  120. model Vehicle {
  121. id String @id @default(cuid())
  122. make String
  123. model String
  124. year Int
  125. vin String?
  126. licensePlate String?
  127. color String?
  128. mileage Int @default(0)
  129. fuelType String? @default("gasoline")
  130. transmission String? @default("automatic")
  131. engineSize String?
  132. engineCode String?
  133. purchaseDate DateTime?
  134. purchasePrice Float?
  135. imageUrl String?
  136. isArchived Boolean @default(false)
  137. archiveReason String?
  138. maintenanceDismissed Boolean @default(false)
  139. maintenanceDismissedAt DateTime?
  140. aiMessages AiGeneratedMessage[]
  141. createdAt DateTime @default(now())
  142. updatedAt DateTime @updatedAt
  143. userId String
  144. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  145. organizationId String?
  146. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  147. customerId String?
  148. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  149. serviceRecords ServiceRecord[]
  150. notes Note[]
  151. fuelLogs FuelLog[]
  152. reminders Reminder[]
  153. quotes Quote[]
  154. recurringInvoices RecurringInvoice[]
  155. inspections Inspection[]
  156. serviceRequests ServiceRequest[]
  157. findings VehicleFinding[]
  158. scheduledMessages ScheduledMessage[]
  159. @@index([organizationId])
  160. @@map("vehicles")
  161. }
  162. model ServiceRecord {
  163. id String @id @default(cuid())
  164. title String
  165. description String?
  166. type String @default("maintenance")
  167. status String @default("completed")
  168. cost Float @default(0)
  169. mileage Int?
  170. serviceDate DateTime @default(now())
  171. startDateTime DateTime?
  172. endDateTime DateTime?
  173. shopName String?
  174. techName String?
  175. parts String?
  176. laborHours Float?
  177. diagnosticNotes String?
  178. invoiceNotes String?
  179. subtotal Float @default(0)
  180. taxRate Float @default(0)
  181. taxAmount Float @default(0)
  182. taxInclusive Boolean @default(false)
  183. totalAmount Float @default(0)
  184. invoiceNumber String?
  185. invoiceDate DateTime?
  186. invoiceDueDate DateTime?
  187. discountType String?
  188. discountValue Float @default(0)
  189. discountAmount Float @default(0)
  190. manuallyPaid Boolean @default(false)
  191. // Warranty tracking
  192. warrantyMonths Int?
  193. warrantyMileage Int?
  194. warrantyExpiresAt DateTime?
  195. warrantyNotes String?
  196. publicToken String? @unique
  197. sharedAt DateTime?
  198. viewCount Int @default(0)
  199. lastViewedAt DateTime?
  200. createdAt DateTime @default(now())
  201. updatedAt DateTime @updatedAt
  202. /// Null for parts-only / counter sales; such records use customerId
  203. /// (or neither, for anonymous walk-in sales)
  204. vehicleId String?
  205. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  206. /// Direct customer for records without a vehicle. Vehicle-linked records
  207. /// leave this null and resolve the customer through the vehicle, so
  208. /// reassigning a vehicle keeps invoices following the vehicle's customer.
  209. customerId String?
  210. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  211. /// Set when the job came straight from an inspection, so the inspection can
  212. /// show what was raised from it. SetNull: deleting the inspection must not
  213. /// take the job — and its invoice — with it.
  214. inspectionId String?
  215. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  216. /// Denormalized from the vehicle historically; now the canonical scope so
  217. /// records exist without a vehicle. Nullable to mirror Vehicle.organizationId.
  218. organizationId String?
  219. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  220. technicianId String?
  221. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  222. sortOrder Int @default(0)
  223. partItems ServicePart[]
  224. laborItems ServiceLabor[]
  225. attachments ServiceAttachment[]
  226. payments Payment[]
  227. statusReports StatusReport[]
  228. stockMovements StockMovement[]
  229. discoveredFindings VehicleFinding[] @relation("FindingDiscoveredIn")
  230. resolvedFindings VehicleFinding[] @relation("FindingResolvedIn")
  231. @@index([vehicleId])
  232. @@index([technicianId])
  233. @@index([organizationId])
  234. @@index([customerId])
  235. @@index([inspectionId])
  236. @@map("service_records")
  237. }
  238. model Note {
  239. id String @id @default(cuid())
  240. title String
  241. content String
  242. isPinned Boolean @default(false)
  243. createdAt DateTime @default(now())
  244. updatedAt DateTime @updatedAt
  245. vehicleId String
  246. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  247. @@index([vehicleId])
  248. @@map("notes")
  249. }
  250. model FuelLog {
  251. id String @id @default(cuid())
  252. date DateTime @default(now())
  253. mileage Int
  254. gallons Float
  255. pricePerGallon Float
  256. totalCost Float
  257. isFillUp Boolean @default(true)
  258. station String?
  259. notes String?
  260. createdAt DateTime @default(now())
  261. updatedAt DateTime @updatedAt
  262. vehicleId String
  263. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  264. @@index([vehicleId])
  265. @@map("fuel_logs")
  266. }
  267. model Reminder {
  268. id String @id @default(cuid())
  269. title String
  270. description String?
  271. dueDate DateTime?
  272. dueMileage Int?
  273. isCompleted Boolean @default(false)
  274. /// Per-reminder notification channels, chosen when creating the reminder
  275. notifyInApp Boolean @default(true)
  276. notifyEmail Boolean @default(false)
  277. /// Set by the due-reminder cron once notified; cleared when dueDate changes
  278. notifiedAt DateTime?
  279. createdAt DateTime @default(now())
  280. updatedAt DateTime @updatedAt
  281. /// A reminder relates to a vehicle, a customer, or just the workshop
  282. /// (both null). organizationId is the canonical scope for all three.
  283. vehicleId String?
  284. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  285. customerId String?
  286. customer Customer? @relation(fields: [customerId], references: [id], onDelete: Cascade)
  287. organizationId String?
  288. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  289. @@index([vehicleId])
  290. @@index([customerId])
  291. @@index([organizationId])
  292. @@map("reminders")
  293. }
  294. model VehicleFinding {
  295. id String @id @default(cuid())
  296. description String
  297. severity String @default("needs_work")
  298. status String @default("open")
  299. notes String?
  300. imageUrls String[] @default([])
  301. createdAt DateTime @default(now())
  302. updatedAt DateTime @updatedAt
  303. vehicleId String
  304. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  305. serviceRecordId String?
  306. serviceRecord ServiceRecord? @relation("FindingDiscoveredIn", fields: [serviceRecordId], references: [id], onDelete: SetNull)
  307. resolvedServiceRecordId String?
  308. resolvedServiceRecord ServiceRecord? @relation("FindingResolvedIn", fields: [resolvedServiceRecordId], references: [id], onDelete: SetNull)
  309. @@index([vehicleId])
  310. @@index([serviceRecordId])
  311. @@map("vehicle_findings")
  312. }
  313. model Customer {
  314. id String @id @default(cuid())
  315. /// Human-facing customer number: auto-assigned sequentially on create,
  316. /// editable afterwards. Unique per organization (nulls allowed for
  317. /// legacy customers).
  318. customerNumber String?
  319. name String
  320. email String?
  321. phone String?
  322. address String?
  323. company String?
  324. taxId String?
  325. taxExempt Boolean @default(false)
  326. notes String?
  327. telegramChatId String?
  328. createdAt DateTime @default(now())
  329. updatedAt DateTime @updatedAt
  330. userId String
  331. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  332. organizationId String?
  333. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  334. vehicles Vehicle[]
  335. quotes Quote[]
  336. smsMessages SmsMessage[]
  337. telegramMessages TelegramMessage[]
  338. customerSessions CustomerSession[]
  339. scheduledMessages ScheduledMessage[]
  340. serviceRequests ServiceRequest[]
  341. serviceRecords ServiceRecord[]
  342. reminders Reminder[]
  343. @@unique([organizationId, customerNumber])
  344. @@index([organizationId])
  345. @@map("customers")
  346. }
  347. model Payment {
  348. id String @id @default(cuid())
  349. amount Float
  350. date DateTime @default(now())
  351. method String @default("other")
  352. note String?
  353. provider String?
  354. externalId String?
  355. createdAt DateTime @default(now())
  356. updatedAt DateTime @updatedAt
  357. serviceRecordId String
  358. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  359. @@index([serviceRecordId])
  360. @@map("payments")
  361. }
  362. model ServiceAttachment {
  363. id String @id @default(cuid())
  364. fileName String
  365. fileUrl String
  366. fileType String
  367. fileSize Int
  368. category String @default("diagnostic")
  369. description String?
  370. includeInInvoice Boolean @default(true)
  371. createdAt DateTime @default(now())
  372. serviceRecordId String
  373. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  374. @@index([serviceRecordId])
  375. @@map("service_attachments")
  376. }
  377. model ServicePart {
  378. id String @id @default(cuid())
  379. partNumber String?
  380. name String
  381. quantity Float @default(1)
  382. unitPrice Float @default(0)
  383. total Float @default(0)
  384. unitCost Float @default(0)
  385. markupPercent Float @default(0)
  386. inventoryPartId String?
  387. serviceRecordId String
  388. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  389. @@index([serviceRecordId])
  390. @@map("service_parts")
  391. }
  392. model ServiceLabor {
  393. id String @id @default(cuid())
  394. description String
  395. hours Float @default(0)
  396. rate Float @default(0)
  397. total Float @default(0)
  398. pricingType String @default("hourly")
  399. serviceRecordId String
  400. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  401. @@index([serviceRecordId])
  402. @@map("service_labor")
  403. }
  404. model LaborPreset {
  405. id String @id @default(cuid())
  406. name String
  407. description String?
  408. isArchived Boolean @default(false)
  409. createdAt DateTime @default(now())
  410. updatedAt DateTime @updatedAt
  411. userId String
  412. organizationId String
  413. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  414. items LaborPresetItem[]
  415. parts LaborPresetPart[]
  416. @@index([organizationId])
  417. @@map("labor_presets")
  418. }
  419. model LaborPresetItem {
  420. id String @id @default(cuid())
  421. description String
  422. hours Float @default(0)
  423. rate Float @default(0)
  424. pricingType String @default("hourly")
  425. sortOrder Int @default(0)
  426. presetId String
  427. preset LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
  428. @@index([presetId])
  429. @@map("labor_preset_items")
  430. }
  431. model LaborPresetPart {
  432. id String @id @default(cuid())
  433. name String
  434. partNumber String?
  435. quantity Float @default(1)
  436. unitPrice Float @default(0)
  437. inventoryPartId String?
  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_parts")
  443. }
  444. model InventoryPart {
  445. id String @id @default(cuid())
  446. partNumber String?
  447. barcode String?
  448. name String
  449. description String?
  450. category String?
  451. quantity Int @default(0)
  452. minQuantity Int @default(0)
  453. unitCost Float @default(0)
  454. sellPrice Float @default(0)
  455. supplier String?
  456. supplierPhone String?
  457. supplierEmail String?
  458. supplierUrl String?
  459. imageUrl String?
  460. location String?
  461. isArchived Boolean @default(false)
  462. /// Set when a low-stock alert has been sent for the current dip below
  463. /// `minQuantity`, and cleared once stock climbs back above it. This
  464. /// hysteresis is what stops a part that sits at zero from re-alerting on
  465. /// every check — it alerts once per descent, not once per run.
  466. lowStockAlertedAt DateTime?
  467. createdAt DateTime @default(now())
  468. updatedAt DateTime @updatedAt
  469. userId String
  470. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  471. organizationId String?
  472. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  473. gallery StoredImage[] @relation("InventoryPartImages")
  474. movements StockMovement[]
  475. @@unique([organizationId, barcode])
  476. @@index([organizationId])
  477. @@map("inventory_parts")
  478. }
  479. /// Append-only audit trail of every change to InventoryPart.quantity.
  480. ///
  481. /// Written inside the same transaction as the stock mutation itself, so the
  482. /// ledger can never drift from the balance it describes. `quantityAfter` is
  483. /// captured from the atomic `UPDATE ... RETURNING`, giving an exact running
  484. /// balance even under concurrent writes.
  485. model StockMovement {
  486. id String @id @default(cuid())
  487. /// Signed change in whole units. Negative = consumed, positive = restocked.
  488. delta Int
  489. /// Part balance immediately after this movement was applied.
  490. quantityAfter Int
  491. /// Why the stock moved — see StockMovementReason in src/features/inventory/Lib/stockMovement.ts
  492. reason String
  493. note String?
  494. inventoryPartId String
  495. inventoryPart InventoryPart @relation(fields: [inventoryPartId], references: [id], onDelete: Cascade)
  496. /// The job the part was used on. Kept on SetNull so history survives
  497. /// deletion of the service record — "where was this used" outlives the job.
  498. serviceRecordId String?
  499. serviceRecord ServiceRecord? @relation(fields: [serviceRecordId], references: [id], onDelete: SetNull)
  500. /// Denormalised so history still reads correctly after a service record is
  501. /// deleted and serviceRecordId goes null.
  502. serviceRecordLabel String?
  503. userId String?
  504. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  505. organizationId String
  506. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  507. createdAt DateTime @default(now())
  508. @@index([inventoryPartId, createdAt(sort: Desc)])
  509. @@index([organizationId, createdAt(sort: Desc)])
  510. @@index([serviceRecordId])
  511. @@map("stock_movements")
  512. }
  513. model StoredImage {
  514. id String @id @default(cuid())
  515. url String
  516. fileName String?
  517. description String?
  518. sortOrder Int @default(0)
  519. createdAt DateTime @default(now())
  520. inventoryPartId String?
  521. inventoryPart InventoryPart? @relation("InventoryPartImages", fields: [inventoryPartId], references: [id], onDelete: Cascade)
  522. @@index([inventoryPartId])
  523. @@map("stored_images")
  524. }
  525. model Quote {
  526. id String @id @default(cuid())
  527. quoteNumber String?
  528. title String
  529. description String?
  530. status String @default("draft")
  531. validUntil DateTime?
  532. subtotal Float @default(0)
  533. taxRate Float @default(0)
  534. taxAmount Float @default(0)
  535. taxInclusive Boolean @default(false)
  536. discountType String?
  537. discountValue Float @default(0)
  538. discountAmount Float @default(0)
  539. totalAmount Float @default(0)
  540. notes String?
  541. publicToken String?
  542. sharedAt DateTime?
  543. viewCount Int @default(0)
  544. lastViewedAt DateTime?
  545. customerMessage String?
  546. convertedToId String?
  547. createdAt DateTime @default(now())
  548. updatedAt DateTime @updatedAt
  549. userId String
  550. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  551. organizationId String?
  552. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  553. customerId String?
  554. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  555. vehicleId String?
  556. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  557. inspectionId String?
  558. inspection Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
  559. partItems QuotePart[]
  560. laborItems QuoteLabor[]
  561. attachments QuoteAttachment[]
  562. @@index([organizationId])
  563. @@map("quotes")
  564. }
  565. model QuoteAttachment {
  566. id String @id @default(cuid())
  567. fileName String
  568. fileUrl String
  569. fileType String
  570. fileSize Int
  571. category String @default("image")
  572. description String?
  573. includeInInvoice Boolean @default(true)
  574. createdAt DateTime @default(now())
  575. quoteId String
  576. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  577. @@index([quoteId])
  578. @@map("quote_attachments")
  579. }
  580. model QuotePart {
  581. id String @id @default(cuid())
  582. partNumber String?
  583. name String
  584. quantity Float @default(1)
  585. /// Internal purchase cost — never shown to customers (quote PDF/share
  586. /// render unitPrice only)
  587. unitCost Float @default(0)
  588. markupPercent Float @default(0)
  589. unitPrice Float @default(0)
  590. total Float @default(0)
  591. excluded Boolean @default(false)
  592. /// Optional link to the stocked part this line was picked from. Carried into
  593. /// ServicePart on conversion so the job deducts the right inventory item.
  594. inventoryPartId String?
  595. quoteId String
  596. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  597. @@index([quoteId])
  598. @@map("quote_parts")
  599. }
  600. model QuoteLabor {
  601. id String @id @default(cuid())
  602. description String
  603. hours Float @default(0)
  604. rate Float @default(0)
  605. total Float @default(0)
  606. pricingType String @default("hourly")
  607. excluded Boolean @default(false)
  608. quoteId String
  609. quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
  610. @@index([quoteId])
  611. @@map("quote_labor")
  612. }
  613. model CustomFieldDefinition {
  614. id String @id @default(cuid())
  615. name String
  616. label String
  617. fieldType String @default("text")
  618. options String?
  619. required Boolean @default(false)
  620. entityType String
  621. sortOrder Int @default(0)
  622. isActive Boolean @default(true)
  623. createdAt DateTime @default(now())
  624. updatedAt DateTime @updatedAt
  625. userId String
  626. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  627. organizationId String?
  628. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  629. values CustomFieldValue[]
  630. @@unique([organizationId, name, entityType])
  631. @@index([organizationId])
  632. @@map("custom_field_definitions")
  633. }
  634. model CustomFieldValue {
  635. id String @id @default(cuid())
  636. value String
  637. entityId String
  638. entityType String
  639. fieldId String
  640. field CustomFieldDefinition @relation(fields: [fieldId], references: [id], onDelete: Cascade)
  641. @@unique([fieldId, entityId])
  642. @@index([fieldId])
  643. @@map("custom_field_values")
  644. }
  645. /// A user-defined dashboard card: a named, filtered table over one entity.
  646. /// The config shape (entity, filters, columns, limit) is validated against
  647. /// the field registry in customCardActions — nothing here reaches SQL raw.
  648. model DashboardWidget {
  649. id String @id @default(cuid())
  650. name String
  651. config Json
  652. userId String
  653. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  654. organizationId String
  655. createdAt DateTime @default(now())
  656. updatedAt DateTime @updatedAt
  657. @@index([userId, organizationId])
  658. @@map("dashboard_widgets")
  659. }
  660. model Organization {
  661. id String @id @default(cuid())
  662. name String
  663. portalSlug String? @unique
  664. createdAt DateTime @default(now())
  665. updatedAt DateTime @updatedAt
  666. members OrganizationMember[]
  667. roles Role[]
  668. vehicles Vehicle[]
  669. serviceRecords ServiceRecord[]
  670. reminders Reminder[]
  671. customers Customer[]
  672. quotes Quote[]
  673. inventoryParts InventoryPart[]
  674. laborPresets LaborPreset[]
  675. customFieldDefinitions CustomFieldDefinition[]
  676. settings AppSetting[]
  677. subscription Subscription?
  678. invitations TeamInvitation[]
  679. inspectionTemplates InspectionTemplate[]
  680. smsMessages SmsMessage[]
  681. telegramMessages TelegramMessage[]
  682. technicians Technician[]
  683. auditLogs AuditLog[]
  684. statusReports StatusReport[]
  685. reportSchedules ReportSchedule[]
  686. scheduledMessages ScheduledMessage[]
  687. webhooks Webhook[]
  688. stockMovements StockMovement[]
  689. @@map("organizations")
  690. }
  691. model OrganizationMember {
  692. id String @id @default(cuid())
  693. role String @default("member")
  694. userId String
  695. user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  696. organizationId String
  697. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  698. roleId String?
  699. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  700. @@unique([userId, organizationId])
  701. @@map("organization_members")
  702. }
  703. model Role {
  704. id String @id @default(cuid())
  705. name String
  706. isAdmin Boolean @default(false)
  707. createdAt DateTime @default(now())
  708. updatedAt DateTime @updatedAt
  709. organizationId String
  710. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  711. permissions Permission[]
  712. members OrganizationMember[]
  713. teamInvitations TeamInvitation[]
  714. @@unique([organizationId, name])
  715. @@map("roles")
  716. }
  717. model Permission {
  718. id String @id @default(cuid())
  719. action String
  720. subject String
  721. roleId String
  722. role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
  723. @@unique([roleId, action, subject])
  724. @@index([roleId])
  725. @@map("permissions")
  726. }
  727. model SystemSetting {
  728. id String @id @default(cuid())
  729. key String @unique
  730. value String
  731. @@map("system_settings")
  732. }
  733. model TeamInvitation {
  734. id String @id @default(cuid())
  735. email String
  736. role String @default("member")
  737. token String @unique
  738. status String @default("pending") // pending | accepted | cancelled
  739. expiresAt DateTime
  740. createdAt DateTime @default(now())
  741. organizationId String
  742. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  743. invitedById String
  744. roleId String?
  745. customRole Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
  746. @@unique([email, organizationId])
  747. @@index([token])
  748. @@map("team_invitations")
  749. }
  750. // Stripe-ready models (implementation in follow-up)
  751. model SubscriptionPlan {
  752. id String @id @default(cuid())
  753. name String
  754. stripePriceId String? @unique
  755. price Float @default(0)
  756. interval String @default("month") // "month" | "year"
  757. maxMembers Int @default(1)
  758. features String? // JSON feature flags
  759. isActive Boolean @default(true)
  760. createdAt DateTime @default(now())
  761. updatedAt DateTime @updatedAt
  762. subscriptions Subscription[]
  763. @@map("subscription_plans")
  764. }
  765. model Subscription {
  766. id String @id @default(cuid())
  767. status String @default("active") // active | canceled | past_due | trialing
  768. stripeSubscriptionId String? @unique
  769. stripeCustomerId String?
  770. currentPeriodStart DateTime?
  771. currentPeriodEnd DateTime?
  772. cancelAtPeriodEnd Boolean @default(false)
  773. createdAt DateTime @default(now())
  774. updatedAt DateTime @updatedAt
  775. organizationId String @unique
  776. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  777. planId String
  778. plan SubscriptionPlan @relation(fields: [planId], references: [id])
  779. @@map("subscriptions")
  780. }
  781. model RecurringInvoice {
  782. id String @id @default(cuid())
  783. title String
  784. description String?
  785. frequency String // "weekly" | "biweekly" | "monthly" | "quarterly" | "yearly"
  786. nextRunDate DateTime
  787. endDate DateTime?
  788. isActive Boolean @default(true)
  789. lastRunAt DateTime?
  790. runCount Int @default(0)
  791. // Template fields (copied to ServiceRecord on generation)
  792. type String @default("maintenance")
  793. cost Float @default(0)
  794. taxRate Float @default(0)
  795. taxInclusive Boolean @default(false)
  796. invoiceNotes String?
  797. vehicleId String
  798. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  799. createdAt DateTime @default(now())
  800. updatedAt DateTime @updatedAt
  801. templateParts RecurringPart[]
  802. templateLabor RecurringLabor[]
  803. @@index([vehicleId])
  804. @@index([nextRunDate, isActive])
  805. @@map("recurring_invoices")
  806. }
  807. model RecurringPart {
  808. id String @id @default(cuid())
  809. name String
  810. partNumber String?
  811. quantity Int @default(1)
  812. unitPrice Float @default(0)
  813. recurringInvoiceId String
  814. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  815. @@map("recurring_parts")
  816. }
  817. model RecurringLabor {
  818. id String @id @default(cuid())
  819. description String
  820. hours Float @default(0)
  821. rate Float @default(0)
  822. pricingType String @default("hourly")
  823. recurringInvoiceId String
  824. recurringInvoice RecurringInvoice @relation(fields: [recurringInvoiceId], references: [id], onDelete: Cascade)
  825. @@map("recurring_labor")
  826. }
  827. model InspectionTemplate {
  828. id String @id @default(cuid())
  829. name String
  830. description String?
  831. isDefault Boolean @default(false)
  832. createdAt DateTime @default(now())
  833. updatedAt DateTime @updatedAt
  834. // Regulatory profile. `country` is an ISO-3166 alpha-2 code (or null for a
  835. // shop-specific checklist), `standard` names the regime the template follows
  836. // (e.g. "eu-2014-45"), and `severityScale` picks which defect vocabulary the
  837. // technician grades against — see Lib/conditions.ts.
  838. country String?
  839. standard String? @default("custom")
  840. severityScale String @default("eu")
  841. /// Provenance, for templates that came from a package rather than being
  842. /// written here. `packageId` is namespaced and stable across renames
  843. /// ("torqvoice/eu-roadworthiness"), which is what lets the library tell an
  844. /// update from a duplicate. Null on a template the workshop wrote itself.
  845. packageId String?
  846. packageVersion String?
  847. /// Where it came from: "builtin", "file" or "catalogue".
  848. packageSource String?
  849. organizationId String
  850. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  851. sections InspectionTemplateSection[]
  852. inspections Inspection[]
  853. @@index([organizationId])
  854. @@index([organizationId, packageId])
  855. @@map("inspection_templates")
  856. }
  857. model InspectionTemplateSection {
  858. id String @id @default(cuid())
  859. name String
  860. description String?
  861. // Regulation reference for the section, e.g. "1" for Braking equipment in
  862. // Directive 2014/45/EU Annex I.
  863. code String?
  864. sortOrder Int @default(0)
  865. templateId String
  866. template InspectionTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
  867. items InspectionTemplateItem[]
  868. @@index([templateId])
  869. @@map("inspection_template_sections")
  870. }
  871. model InspectionTemplateItem {
  872. id String @id @default(cuid())
  873. name String
  874. description String?
  875. // Regulation reference for the check, e.g. "1.1.13".
  876. code String?
  877. sortOrder Int @default(0)
  878. // How the technician records the result: "condition" (defect grading only),
  879. // "measurement" (numeric value in `unit`, graded against min/max),
  880. // "text" (free text) or "choice" (one of `choices`).
  881. inputType String @default("condition")
  882. unit String?
  883. minValue Float?
  884. maxValue Float?
  885. choices String[] @default([])
  886. required Boolean @default(false)
  887. photoRequired Boolean @default(false)
  888. // Defect severity applied when a measurement falls outside min/max.
  889. defaultSeverity String?
  890. // Phrases this workshop wants offered when a defect is recorded against the
  891. // check, on top of the built-in catalogue. See Lib/defectCatalogue.ts.
  892. defectSuggestions String[] @default([])
  893. sectionId String
  894. section InspectionTemplateSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
  895. @@index([sectionId])
  896. @@map("inspection_template_items")
  897. }
  898. model Inspection {
  899. id String @id @default(cuid())
  900. status String @default("in_progress")
  901. mileage Int?
  902. notes String?
  903. startDateTime DateTime?
  904. endDateTime DateTime?
  905. publicToken String? @unique
  906. completedAt DateTime?
  907. createdAt DateTime @default(now())
  908. updatedAt DateTime @updatedAt
  909. vehicleId String
  910. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  911. templateId String
  912. template InspectionTemplate @relation(fields: [templateId], references: [id])
  913. technicianId String?
  914. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  915. sortOrder Int @default(0)
  916. // Roadworthiness certificate fields required by Directive 2014/45/EU Annex IV.
  917. // `inspectorName` and `certificateNumber` are snapshots taken at completion so
  918. // the issued certificate stays stable even if staff or settings change later.
  919. /// Snapshot of how this inspection was graded, taken when it was created.
  920. /// Read in preference to the template's current setting: a workshop that
  921. /// later switches a template to the EU scale must not relabel a certificate
  922. /// it already issued. Null on inspections that predate the snapshot; those
  923. /// fall back to the template, which the migration pinned to its old scale.
  924. severityScale String?
  925. country String?
  926. vehicleCategory String?
  927. nextTestDue DateTime?
  928. certificateNumber String?
  929. inspectorName String?
  930. testLocation String?
  931. organizationId String
  932. items InspectionItem[]
  933. quoteRequests InspectionQuoteRequest[]
  934. quotes Quote[]
  935. serviceRecords ServiceRecord[]
  936. @@index([vehicleId])
  937. @@index([organizationId])
  938. @@index([technicianId])
  939. @@map("inspections")
  940. }
  941. model InspectionItem {
  942. id String @id @default(cuid())
  943. name String
  944. section String
  945. sortOrder Int @default(0)
  946. condition String @default("not_inspected")
  947. notes String?
  948. imageUrls String[] @default([])
  949. // Copied from the template at creation so an inspection stays readable even
  950. // after its template is edited or deleted.
  951. description String?
  952. code String?
  953. sectionCode String?
  954. inputType String @default("condition")
  955. unit String?
  956. minValue Float?
  957. maxValue Float?
  958. choices String[] @default([])
  959. required Boolean @default(false)
  960. photoRequired Boolean @default(false)
  961. defaultSeverity String?
  962. defectSuggestions String[] @default([])
  963. measuredValue Float?
  964. textValue String?
  965. inspectionId String
  966. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  967. @@index([inspectionId])
  968. @@map("inspection_items")
  969. }
  970. model InspectionQuoteRequest {
  971. id String @id @default(cuid())
  972. status String @default("pending")
  973. message String?
  974. selectedItemIds String[]
  975. createdAt DateTime @default(now())
  976. inspectionId String
  977. inspection Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
  978. organizationId String
  979. @@index([organizationId])
  980. @@index([inspectionId])
  981. @@map("inspection_quote_requests")
  982. }
  983. model Notification {
  984. id String @id @default(cuid())
  985. type String
  986. title String
  987. message String
  988. entityType String
  989. entityId String
  990. entityUrl String
  991. read Boolean @default(false)
  992. organizationId String
  993. createdAt DateTime @default(now())
  994. @@index([organizationId, createdAt(sort: Desc)])
  995. @@index([organizationId, read])
  996. @@map("notifications")
  997. }
  998. model SmsMessage {
  999. id String @id @default(cuid())
  1000. direction String // "inbound" | "outbound"
  1001. fromNumber String
  1002. toNumber String
  1003. body String
  1004. status String @default("queued") // queued | sent | delivered | failed | received
  1005. providerMsgId String?
  1006. errorMessage String?
  1007. relatedEntityType String?
  1008. relatedEntityId String?
  1009. createdAt DateTime @default(now())
  1010. updatedAt DateTime @updatedAt
  1011. organizationId String
  1012. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1013. customerId String?
  1014. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1015. @@index([organizationId, customerId, createdAt(sort: Desc)])
  1016. @@index([organizationId, createdAt(sort: Desc)])
  1017. @@index([toNumber, organizationId])
  1018. @@map("sms_messages")
  1019. }
  1020. model TelegramMessage {
  1021. id String @id @default(cuid())
  1022. direction String // "inbound" | "outbound"
  1023. chatId String // Telegram chat ID
  1024. body String
  1025. status String @default("queued") // queued | sent | delivered | failed | received
  1026. telegramMessageId String? // Telegram's message ID for tracking
  1027. errorMessage String?
  1028. relatedEntityType String?
  1029. relatedEntityId String?
  1030. createdAt DateTime @default(now())
  1031. updatedAt DateTime @updatedAt
  1032. organizationId String
  1033. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1034. customerId String?
  1035. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1036. @@index([organizationId, customerId, createdAt(sort: Desc)])
  1037. @@index([organizationId, chatId, createdAt(sort: Desc)])
  1038. @@map("telegram_messages")
  1039. }
  1040. model Technician {
  1041. id String @id @default(cuid())
  1042. name String
  1043. color String @default("#3b82f6")
  1044. isActive Boolean @default(true)
  1045. sortOrder Int @default(0)
  1046. dailyCapacity Int @default(480)
  1047. userId String?
  1048. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  1049. organizationId String
  1050. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1051. serviceRecords ServiceRecord[]
  1052. inspections Inspection[]
  1053. statusReports StatusReport[]
  1054. createdAt DateTime @default(now())
  1055. updatedAt DateTime @updatedAt
  1056. @@index([organizationId])
  1057. @@map("technicians")
  1058. }
  1059. model AuditLog {
  1060. id String @id @default(cuid())
  1061. timestamp DateTime @default(now())
  1062. action String
  1063. entity String?
  1064. entityId String?
  1065. message String?
  1066. metadata Json?
  1067. ip String?
  1068. userAgent String?
  1069. userId String?
  1070. user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
  1071. organizationId String?
  1072. organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
  1073. @@index([organizationId, timestamp(sort: Desc)])
  1074. @@index([userId, timestamp(sort: Desc)])
  1075. @@index([action, timestamp(sort: Desc)])
  1076. @@map("audit_logs")
  1077. }
  1078. model CustomerSession {
  1079. id String @id @default(cuid())
  1080. token String @unique
  1081. expiresAt DateTime
  1082. createdAt DateTime @default(now())
  1083. customerId String
  1084. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1085. organizationId String
  1086. @@index([token])
  1087. @@index([customerId, organizationId])
  1088. @@map("customer_sessions")
  1089. }
  1090. model CustomerMagicLink {
  1091. id String @id @default(cuid())
  1092. token String @unique
  1093. email String
  1094. organizationId String
  1095. expiresAt DateTime
  1096. usedAt DateTime?
  1097. createdAt DateTime @default(now())
  1098. @@index([token])
  1099. @@map("customer_magic_links")
  1100. }
  1101. model CustomerSmsCode {
  1102. id String @id @default(cuid())
  1103. code String
  1104. phone String
  1105. organizationId String
  1106. expiresAt DateTime
  1107. usedAt DateTime?
  1108. createdAt DateTime @default(now())
  1109. @@index([phone, organizationId])
  1110. @@index([organizationId])
  1111. @@map("customer_sms_codes")
  1112. }
  1113. model ServiceRequest {
  1114. id String @id @default(cuid())
  1115. description String
  1116. preferredDate DateTime?
  1117. status String @default("pending")
  1118. adminNotes String?
  1119. createdAt DateTime @default(now())
  1120. updatedAt DateTime @updatedAt
  1121. customerId String
  1122. customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  1123. vehicleId String
  1124. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1125. organizationId String
  1126. @@index([organizationId, status])
  1127. @@index([customerId])
  1128. @@map("service_requests")
  1129. }
  1130. model AiGeneratedMessage {
  1131. id String @id @default(cuid())
  1132. vehicleId String
  1133. type String // "summary" | "common_issues" | etc.
  1134. content String
  1135. createdAt DateTime @default(now())
  1136. updatedAt DateTime @updatedAt
  1137. vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
  1138. @@unique([vehicleId, type])
  1139. @@map("ai_generated_messages")
  1140. }
  1141. model AiChat {
  1142. id String @id @default(cuid())
  1143. title String @default("New chat")
  1144. createdAt DateTime @default(now())
  1145. updatedAt DateTime @updatedAt
  1146. userId String
  1147. organizationId String
  1148. messages AiChatMessage[]
  1149. @@index([organizationId, userId, updatedAt(sort: Desc)])
  1150. @@map("ai_chats")
  1151. }
  1152. model AiChatMessage {
  1153. id String @id @default(cuid())
  1154. role String // "user" | "assistant"
  1155. content String
  1156. createdAt DateTime @default(now())
  1157. chatId String
  1158. chat AiChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
  1159. @@index([chatId, createdAt])
  1160. @@map("ai_chat_messages")
  1161. }
  1162. model StatusReport {
  1163. id String @id @default(cuid())
  1164. title String?
  1165. message String? @db.Text
  1166. videoUrl String?
  1167. videoFileName String?
  1168. publicToken String @unique
  1169. status String @default("draft") // draft | published | sent | viewed
  1170. sentVia String? // email, sms, telegram (comma-separated)
  1171. sentAt DateTime?
  1172. viewedAt DateTime?
  1173. customerFeedback String? @db.Text
  1174. feedbackAt DateTime?
  1175. expiresAt DateTime?
  1176. createdAt DateTime @default(now())
  1177. updatedAt DateTime @updatedAt
  1178. serviceRecordId String
  1179. serviceRecord ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
  1180. organizationId String
  1181. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1182. technicianId String?
  1183. technician Technician? @relation(fields: [technicianId], references: [id], onDelete: SetNull)
  1184. @@index([serviceRecordId])
  1185. @@index([organizationId])
  1186. @@index([publicToken])
  1187. @@index([expiresAt])
  1188. @@map("status_reports")
  1189. }
  1190. model ReportSchedule {
  1191. id String @id @default(cuid())
  1192. name String @default("Scheduled Report")
  1193. frequency String // "daily" | "weekly" | "monthly" etc.
  1194. dateRange String @default("last30d") // "last1d" | "last7d" | "last30d" | "ytd" | "allTime" etc.
  1195. sections String // JSON array of section keys
  1196. recipients String // JSON array of user IDs
  1197. nextRunDate DateTime
  1198. endDate DateTime?
  1199. isActive Boolean @default(true)
  1200. lastRunAt DateTime?
  1201. runCount Int @default(0)
  1202. createdAt DateTime @default(now())
  1203. updatedAt DateTime @updatedAt
  1204. organizationId String
  1205. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1206. createdById String
  1207. @@index([organizationId])
  1208. @@index([nextRunDate, isActive])
  1209. @@map("report_schedules")
  1210. }
  1211. /// A message the workshop lines up to go out on a given day and time, through
  1212. /// whichever channel suits the customer. One-off by default; a frequency other
  1213. /// than "once" reschedules itself after each send, the way ReportSchedule does.
  1214. model ScheduledMessage {
  1215. id String @id @default(cuid())
  1216. channel String // "email" | "sms" | "telegram" | "in_app"
  1217. subject String? // email only
  1218. body String
  1219. /// Where it goes when no customer is attached, or when the workshop typed an
  1220. /// address by hand. Falls back to the customer's email/phone/chat id.
  1221. recipient String?
  1222. status String @default("scheduled") // scheduled | sent | failed | cancelled
  1223. sendAt DateTime
  1224. frequency String @default("once") // once | daily | weekly | biweekly | monthly | yearly
  1225. endDate DateTime?
  1226. lastRunAt DateTime?
  1227. sentAt DateTime?
  1228. runCount Int @default(0)
  1229. errorMessage String?
  1230. createdAt DateTime @default(now())
  1231. updatedAt DateTime @updatedAt
  1232. organizationId String
  1233. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1234. customerId String?
  1235. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  1236. vehicleId String?
  1237. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  1238. createdById String
  1239. @@index([organizationId, sendAt])
  1240. @@index([status, sendAt])
  1241. @@map("scheduled_messages")
  1242. }
  1243. model Webhook {
  1244. id String @id @default(cuid())
  1245. name String
  1246. url String
  1247. secret String
  1248. events String // JSON array of event names; "*" matches all
  1249. isActive Boolean @default(true)
  1250. description String?
  1251. lastTriggeredAt DateTime?
  1252. lastSuccessAt DateTime?
  1253. lastFailureAt DateTime?
  1254. failureCount Int @default(0)
  1255. createdAt DateTime @default(now())
  1256. updatedAt DateTime @updatedAt
  1257. organizationId String
  1258. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  1259. createdById String
  1260. deliveries WebhookDelivery[]
  1261. @@index([organizationId])
  1262. @@index([organizationId, isActive])
  1263. @@map("webhooks")
  1264. }
  1265. model WebhookDelivery {
  1266. id String @id @default(cuid())
  1267. event String
  1268. payload String // JSON-stringified event payload as sent
  1269. status String @default("pending") // pending | success | failed | retrying
  1270. statusCode Int?
  1271. responseBody String?
  1272. errorMessage String?
  1273. attempt Int @default(0)
  1274. maxAttempts Int @default(5)
  1275. nextRetryAt DateTime?
  1276. deliveredAt DateTime?
  1277. durationMs Int?
  1278. createdAt DateTime @default(now())
  1279. updatedAt DateTime @updatedAt
  1280. webhookId String
  1281. webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
  1282. @@index([webhookId, createdAt(sort: Desc)])
  1283. @@index([status, nextRetryAt])
  1284. @@map("webhook_deliveries")
  1285. }