messaging.prisma 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. model Notification {
  2. id String @id @default(cuid())
  3. type String
  4. title String
  5. message String
  6. entityType String
  7. entityId String
  8. entityUrl String
  9. read Boolean @default(false)
  10. organizationId String
  11. createdAt DateTime @default(now())
  12. @@index([organizationId, createdAt(sort: Desc)])
  13. @@index([organizationId, read])
  14. @@map("notifications")
  15. }
  16. model SmsMessage {
  17. id String @id @default(cuid())
  18. direction String // "inbound" | "outbound"
  19. fromNumber String
  20. toNumber String
  21. body String
  22. status String @default("queued") // queued | sent | delivered | failed | received
  23. providerMsgId String?
  24. errorMessage String?
  25. relatedEntityType String?
  26. relatedEntityId String?
  27. /// When the workshop first opened the thread this arrived in. Null on an
  28. /// inbound message nobody has read yet; never set on an outbound row.
  29. readAt DateTime?
  30. createdAt DateTime @default(now())
  31. updatedAt DateTime @updatedAt
  32. organizationId String
  33. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  34. customerId String?
  35. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  36. @@index([organizationId, customerId, createdAt(sort: Desc)])
  37. @@index([organizationId, createdAt(sort: Desc)])
  38. @@index([toNumber, organizationId])
  39. @@index([organizationId, readAt])
  40. @@map("sms_messages")
  41. }
  42. model TelegramMessage {
  43. id String @id @default(cuid())
  44. direction String // "inbound" | "outbound"
  45. chatId String // Telegram chat ID
  46. body String
  47. status String @default("queued") // queued | sent | delivered | failed | received
  48. telegramMessageId String? // Telegram's message ID for tracking
  49. errorMessage String?
  50. relatedEntityType String?
  51. relatedEntityId String?
  52. /// When the workshop first opened the thread this arrived in. Null on an
  53. /// inbound message nobody has read yet; never set on an outbound row.
  54. readAt DateTime?
  55. createdAt DateTime @default(now())
  56. updatedAt DateTime @updatedAt
  57. organizationId String
  58. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  59. customerId String?
  60. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  61. @@index([organizationId, customerId, createdAt(sort: Desc)])
  62. @@index([organizationId, chatId, createdAt(sort: Desc)])
  63. @@index([organizationId, readAt])
  64. @@map("telegram_messages")
  65. }
  66. /// One WhatsApp message in either direction.
  67. ///
  68. /// `provider` records which adapter carried it, because a workshop can switch
  69. /// provider without its history becoming unreadable, and a status callback
  70. /// arriving later has to find the row it belongs to.
  71. model WhatsappMessage {
  72. id String @id @default(cuid())
  73. direction String // "inbound" | "outbound"
  74. provider String // "meta" | "twilio" | any adapter registered later
  75. /// E.164 without the "whatsapp:" prefix a provider may use on the wire.
  76. fromNumber String
  77. toNumber String
  78. body String?
  79. /// Media the message carried. Inbound URLs may need the provider's
  80. /// credentials to fetch, so they are not necessarily public.
  81. mediaUrl String?
  82. mediaType String? // image | document | video | audio | sticker
  83. mediaFilename String?
  84. /// Set when the message had to go out as an approved template, which is the
  85. /// only thing WhatsApp allows outside the 24 hour service window.
  86. templateName String?
  87. status String @default("queued") // queued | sent | delivered | read | failed | received
  88. providerMessageId String?
  89. errorMessage String?
  90. relatedEntityType String?
  91. relatedEntityId String?
  92. /// When the workshop first opened the thread this arrived in. Null on an
  93. /// inbound message nobody has read yet; never set on an outbound row.
  94. readAt DateTime?
  95. createdAt DateTime @default(now())
  96. updatedAt DateTime @updatedAt
  97. organizationId String
  98. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  99. customerId String?
  100. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  101. @@index([organizationId, customerId, createdAt(sort: Desc)])
  102. @@index([organizationId, createdAt(sort: Desc)])
  103. @@index([providerMessageId])
  104. @@index([organizationId, readAt])
  105. @@map("whatsapp_messages")
  106. }
  107. /// A message the workshop lines up to go out on a given day and time, through
  108. /// whichever channel suits the customer. One-off by default; a frequency other
  109. /// than "once" reschedules itself after each send, the way ReportSchedule does.
  110. model ScheduledMessage {
  111. id String @id @default(cuid())
  112. channel String // "email" | "sms" | "telegram" | "in_app"
  113. subject String? // email only
  114. body String
  115. /// Where it goes when no customer is attached, or when the workshop typed an
  116. /// address by hand. Falls back to the customer's email/phone/chat id.
  117. recipient String?
  118. status String @default("scheduled") // scheduled | sent | failed | cancelled
  119. sendAt DateTime
  120. frequency String @default("once") // once | daily | weekly | biweekly | monthly | yearly
  121. endDate DateTime?
  122. lastRunAt DateTime?
  123. sentAt DateTime?
  124. runCount Int @default(0)
  125. errorMessage String?
  126. createdAt DateTime @default(now())
  127. updatedAt DateTime @updatedAt
  128. organizationId String
  129. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  130. customerId String?
  131. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  132. vehicleId String?
  133. vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
  134. createdById String
  135. inspectionReminder InspectionReminderSend?
  136. @@index([organizationId, sendAt])
  137. @@index([status, sendAt])
  138. @@map("scheduled_messages")
  139. }
  140. /// One email between a workshop and a customer, in either direction.
  141. ///
  142. /// Only the cloud instance writes these, and only for a workshop that sends
  143. /// through the platform's address: its customer mail carries a Reply-To of
  144. /// `reply-<token>@<reply domain>`, and what comes back to that address is
  145. /// filed here. A workshop with its own mail vendor gets replies in its own
  146. /// mailbox, so nothing of its mail is recorded.
  147. ///
  148. /// `kind` tells a document mail (an invoice, a quote, a scheduled message)
  149. /// from a message written in the inbox. Document sends are kept so a reply
  150. /// shows what it answers; a thread is listed only once it holds a message,
  151. /// so sending invoices does not fill the inbox with one-sided threads.
  152. model EmailMessage {
  153. id String @id @default(cuid())
  154. direction String // "inbound" | "outbound"
  155. kind String @default("message") // "message" | "document"
  156. /// The customer's address, lower-cased: who wrote in, or who was written to.
  157. address String
  158. /// The display name the sender's mail client gave, on inbound rows.
  159. fromName String?
  160. subject String?
  161. /// Plain text. An HTML-only mail is converted on receipt; HTML is never kept.
  162. body String
  163. /// RFC 5322 Message-ID without the angle brackets: ours on outbound rows,
  164. /// theirs on inbound ones. A reply's In-Reply-To finds the row it answers.
  165. messageId String?
  166. inReplyTo String?
  167. /// The receiving service's own id for an inbound mail, which is also the
  168. /// stored object's key. Unique, so a redelivered notification files nothing.
  169. providerMessageId String? @unique
  170. /// What the receiving service's spam filter said: PASS, FAIL or GRAY.
  171. spamVerdict String?
  172. status String @default("received") // queued | sent | failed | received
  173. errorMessage String?
  174. relatedEntityType String?
  175. relatedEntityId String?
  176. /// When the workshop first opened the thread this arrived in. Null on an
  177. /// inbound message nobody has read yet; never set on an outbound row.
  178. readAt DateTime?
  179. createdAt DateTime @default(now())
  180. updatedAt DateTime @updatedAt
  181. organizationId String
  182. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  183. customerId String?
  184. customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
  185. attachments EmailAttachment[]
  186. @@index([organizationId, customerId, createdAt(sort: Desc)])
  187. @@index([organizationId, address, createdAt(sort: Desc)])
  188. @@index([organizationId, createdAt(sort: Desc)])
  189. @@index([organizationId, readAt])
  190. @@index([organizationId, messageId])
  191. @@map("email_messages")
  192. }
  193. /// A file that came with an inbound email, kept as one of the workshop's
  194. /// uploads (folder `email`) and released through the file manager when the
  195. /// message goes.
  196. model EmailAttachment {
  197. id String @id @default(cuid())
  198. /// The name the sender gave it, cleaned for display; the file on disk has
  199. /// a generated name.
  200. fileName String
  201. contentType String
  202. size Int
  203. url String
  204. createdAt DateTime @default(now())
  205. messageId String
  206. message EmailMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)
  207. @@index([messageId])
  208. @@map("email_attachments")
  209. }