elevenlabs-webhook-nodejs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (969) hide show
  1. package/.claude/settings.local.json +27 -0
  2. package/.dockerignore +6 -0
  3. package/.env.example +43 -0
  4. package/BEFORE-PRODUCTION.md +32 -0
  5. package/Dockerfile +23 -0
  6. package/SYSTEM_OVERVIEW.md +942 -0
  7. package/SYSTEM_STATUS.md +332 -0
  8. package/backup_script/main.py +146 -0
  9. package/baileys/.dockerignore +7 -0
  10. package/baileys/Dockerfile +13 -0
  11. package/baileys/README.md +412 -0
  12. package/baileys/index.js +499 -0
  13. package/baileys/package-lock.json +2532 -0
  14. package/baileys/package.json +25 -0
  15. package/baileys/server.js +96 -0
  16. package/baileys/src/config.js +55 -0
  17. package/baileys/src/middleware/api-key.js +16 -0
  18. package/baileys/src/routes/accounts.js +51 -0
  19. package/baileys/src/routes/chats.js +103 -0
  20. package/baileys/src/routes/webhooks.js +34 -0
  21. package/baileys/src/services/account-store.js +259 -0
  22. package/baileys/src/services/webhook-dispatcher.js +161 -0
  23. package/baileys/src/services/worker-manager.js +597 -0
  24. package/baileys/src/utils/jid.js +79 -0
  25. package/baileys/src/utils/logger.js +16 -0
  26. package/baileys/src/utils/use-mongodb-auth-state.js +122 -0
  27. package/baileys/worker.js +721 -0
  28. package/dist/app.d.ts +1 -0
  29. package/dist/app.js +84 -0
  30. package/dist/app.js.map +1 -0
  31. package/dist/config/agentPrompts.json +19 -0
  32. package/dist/config/database.d.ts +1 -0
  33. package/dist/config/database.js +26 -0
  34. package/dist/config/database.js.map +1 -0
  35. package/dist/config/env.d.ts +41 -0
  36. package/dist/config/env.js +81 -0
  37. package/dist/config/env.js.map +1 -0
  38. package/dist/config/index.d.ts +3 -0
  39. package/dist/config/index.js +73 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/controllers/webhook.controller.d.ts +10 -0
  42. package/dist/controllers/webhook.controller.js +128 -0
  43. package/dist/controllers/webhook.controller.js.map +1 -0
  44. package/dist/errors/index.d.ts +4 -0
  45. package/dist/errors/index.js +13 -0
  46. package/dist/errors/index.js.map +1 -0
  47. package/dist/hooks/webhookValidator.d.ts +2 -0
  48. package/dist/hooks/webhookValidator.js +47 -0
  49. package/dist/hooks/webhookValidator.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +55 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/migrations/001_session-architecture.d.ts +3 -0
  54. package/dist/migrations/001_session-architecture.js +119 -0
  55. package/dist/migrations/001_session-architecture.js.map +1 -0
  56. package/dist/migrations/002_agent-ref.d.ts +3 -0
  57. package/dist/migrations/002_agent-ref.js +55 -0
  58. package/dist/migrations/002_agent-ref.js.map +1 -0
  59. package/dist/migrations/003_shift-schedule.d.ts +3 -0
  60. package/dist/migrations/003_shift-schedule.js +48 -0
  61. package/dist/migrations/003_shift-schedule.js.map +1 -0
  62. package/dist/migrations/004_invert-shift-to-clinic-hours.d.ts +3 -0
  63. package/dist/migrations/004_invert-shift-to-clinic-hours.js +27 -0
  64. package/dist/migrations/004_invert-shift-to-clinic-hours.js.map +1 -0
  65. package/dist/migrations/005_composite-baileys-chat-ids.d.ts +3 -0
  66. package/dist/migrations/005_composite-baileys-chat-ids.js +47 -0
  67. package/dist/migrations/005_composite-baileys-chat-ids.js.map +1 -0
  68. package/dist/migrations/migration.model.d.ts +18 -0
  69. package/dist/migrations/migration.model.js +45 -0
  70. package/dist/migrations/migration.model.js.map +1 -0
  71. package/dist/migrations/migration.routes.d.ts +2 -0
  72. package/dist/migrations/migration.routes.js +37 -0
  73. package/dist/migrations/migration.routes.js.map +1 -0
  74. package/dist/migrations/migration.runner.d.ts +19 -0
  75. package/dist/migrations/migration.runner.js +74 -0
  76. package/dist/migrations/migration.runner.js.map +1 -0
  77. package/dist/models/ConversationClaim.d.ts +13 -0
  78. package/dist/models/ConversationClaim.js +44 -0
  79. package/dist/models/ConversationClaim.js.map +1 -0
  80. package/dist/models/ConversationEvaluation.d.ts +23 -0
  81. package/dist/models/ConversationEvaluation.js +92 -0
  82. package/dist/models/ConversationEvaluation.js.map +1 -0
  83. package/dist/models/Summary.d.ts +37 -0
  84. package/dist/models/Summary.js +78 -0
  85. package/dist/models/Summary.js.map +1 -0
  86. package/dist/models/Transcription.d.ts +34 -0
  87. package/dist/models/Transcription.js +71 -0
  88. package/dist/models/Transcription.js.map +1 -0
  89. package/dist/models/WhatsAppRecipient.d.ts +23 -0
  90. package/dist/models/WhatsAppRecipient.js +53 -0
  91. package/dist/models/WhatsAppRecipient.js.map +1 -0
  92. package/dist/modules/admin/admin.routes.d.ts +2 -0
  93. package/dist/modules/admin/admin.routes.js +1966 -0
  94. package/dist/modules/admin/admin.routes.js.map +1 -0
  95. package/dist/modules/admin/elevenlabs-test-chat.routes.d.ts +2 -0
  96. package/dist/modules/admin/elevenlabs-test-chat.routes.js +158 -0
  97. package/dist/modules/admin/elevenlabs-test-chat.routes.js.map +1 -0
  98. package/dist/modules/admin/whatsapp-test-chat.routes.d.ts +2 -0
  99. package/dist/modules/admin/whatsapp-test-chat.routes.js +204 -0
  100. package/dist/modules/admin/whatsapp-test-chat.routes.js.map +1 -0
  101. package/dist/modules/agents/agent.model.d.ts +38 -0
  102. package/dist/modules/agents/agent.model.js +92 -0
  103. package/dist/modules/agents/agent.model.js.map +1 -0
  104. package/dist/modules/agents/agent.routes.d.ts +2 -0
  105. package/dist/modules/agents/agent.routes.js +61 -0
  106. package/dist/modules/agents/agent.routes.js.map +1 -0
  107. package/dist/modules/appointment-validation/appointment-validation.model.d.ts +76 -0
  108. package/dist/modules/appointment-validation/appointment-validation.model.js +118 -0
  109. package/dist/modules/appointment-validation/appointment-validation.model.js.map +1 -0
  110. package/dist/modules/appointment-validation/appointment-validation.routes.d.ts +2 -0
  111. package/dist/modules/appointment-validation/appointment-validation.routes.js +202 -0
  112. package/dist/modules/appointment-validation/appointment-validation.routes.js.map +1 -0
  113. package/dist/modules/appointment-validation/appointment-validation.service.d.ts +53 -0
  114. package/dist/modules/appointment-validation/appointment-validation.service.js +827 -0
  115. package/dist/modules/appointment-validation/appointment-validation.service.js.map +1 -0
  116. package/dist/modules/auth/auth.model.d.ts +17 -0
  117. package/dist/modules/auth/auth.model.js +64 -0
  118. package/dist/modules/auth/auth.model.js.map +1 -0
  119. package/dist/modules/auth/auth.routes.d.ts +2 -0
  120. package/dist/modules/auth/auth.routes.js +202 -0
  121. package/dist/modules/auth/auth.routes.js.map +1 -0
  122. package/dist/modules/auth/auth.service.d.ts +28 -0
  123. package/dist/modules/auth/auth.service.js +183 -0
  124. package/dist/modules/auth/auth.service.js.map +1 -0
  125. package/dist/modules/auth/refresh-token.model.d.ts +13 -0
  126. package/dist/modules/auth/refresh-token.model.js +52 -0
  127. package/dist/modules/auth/refresh-token.model.js.map +1 -0
  128. package/dist/modules/billing/billing-alert.model.d.ts +16 -0
  129. package/dist/modules/billing/billing-alert.model.js +47 -0
  130. package/dist/modules/billing/billing-alert.model.js.map +1 -0
  131. package/dist/modules/billing/billing-period-snapshot.model.d.ts +35 -0
  132. package/dist/modules/billing/billing-period-snapshot.model.js +68 -0
  133. package/dist/modules/billing/billing-period-snapshot.model.js.map +1 -0
  134. package/dist/modules/billing/billing.model.d.ts +18 -0
  135. package/dist/modules/billing/billing.model.js +62 -0
  136. package/dist/modules/billing/billing.model.js.map +1 -0
  137. package/dist/modules/billing/billing.routes.d.ts +2 -0
  138. package/dist/modules/billing/billing.routes.js +63 -0
  139. package/dist/modules/billing/billing.routes.js.map +1 -0
  140. package/dist/modules/billing/billing.service.d.ts +69 -0
  141. package/dist/modules/billing/billing.service.js +498 -0
  142. package/dist/modules/billing/billing.service.js.map +1 -0
  143. package/dist/modules/billing/payment.model.d.ts +24 -0
  144. package/dist/modules/billing/payment.model.js +57 -0
  145. package/dist/modules/billing/payment.model.js.map +1 -0
  146. package/dist/modules/calls/call.model.d.ts +41 -0
  147. package/dist/modules/calls/call.model.js +97 -0
  148. package/dist/modules/calls/call.model.js.map +1 -0
  149. package/dist/modules/calls/call.routes.d.ts +2 -0
  150. package/dist/modules/calls/call.routes.js +103 -0
  151. package/dist/modules/calls/call.routes.js.map +1 -0
  152. package/dist/modules/campaigns/campaign.model.d.ts +45 -0
  153. package/dist/modules/campaigns/campaign.model.js +98 -0
  154. package/dist/modules/campaigns/campaign.model.js.map +1 -0
  155. package/dist/modules/campaigns/campaign.routes.d.ts +2 -0
  156. package/dist/modules/campaigns/campaign.routes.js +323 -0
  157. package/dist/modules/campaigns/campaign.routes.js.map +1 -0
  158. package/dist/modules/campaigns/campaign.service.d.ts +11 -0
  159. package/dist/modules/campaigns/campaign.service.js +86 -0
  160. package/dist/modules/campaigns/campaign.service.js.map +1 -0
  161. package/dist/modules/google-calendar/google-calendar.routes.d.ts +2 -0
  162. package/dist/modules/google-calendar/google-calendar.routes.js +32 -0
  163. package/dist/modules/google-calendar/google-calendar.routes.js.map +1 -0
  164. package/dist/modules/inbound-call/inbound-call-config.model.d.ts +20 -0
  165. package/dist/modules/inbound-call/inbound-call-config.model.js +68 -0
  166. package/dist/modules/inbound-call/inbound-call-config.model.js.map +1 -0
  167. package/dist/modules/inbound-call/inbound-call.routes.d.ts +2 -0
  168. package/dist/modules/inbound-call/inbound-call.routes.js +243 -0
  169. package/dist/modules/inbound-call/inbound-call.routes.js.map +1 -0
  170. package/dist/modules/leads/lead.model.d.ts +24 -0
  171. package/dist/modules/leads/lead.model.js +54 -0
  172. package/dist/modules/leads/lead.model.js.map +1 -0
  173. package/dist/modules/leads/lead.routes.d.ts +2 -0
  174. package/dist/modules/leads/lead.routes.js +201 -0
  175. package/dist/modules/leads/lead.routes.js.map +1 -0
  176. package/dist/modules/plans/plan.model.d.ts +25 -0
  177. package/dist/modules/plans/plan.model.js +59 -0
  178. package/dist/modules/plans/plan.model.js.map +1 -0
  179. package/dist/modules/surveys/survey.model.d.ts +30 -0
  180. package/dist/modules/surveys/survey.model.js +75 -0
  181. package/dist/modules/surveys/survey.model.js.map +1 -0
  182. package/dist/modules/surveys/survey.routes.d.ts +2 -0
  183. package/dist/modules/surveys/survey.routes.js +153 -0
  184. package/dist/modules/surveys/survey.routes.js.map +1 -0
  185. package/dist/modules/tenants/tenant.model.d.ts +86 -0
  186. package/dist/modules/tenants/tenant.model.js +127 -0
  187. package/dist/modules/tenants/tenant.model.js.map +1 -0
  188. package/dist/modules/tenants/tenant.routes.d.ts +2 -0
  189. package/dist/modules/tenants/tenant.routes.js +65 -0
  190. package/dist/modules/tenants/tenant.routes.js.map +1 -0
  191. package/dist/modules/users/user.routes.d.ts +2 -0
  192. package/dist/modules/users/user.routes.js +106 -0
  193. package/dist/modules/users/user.routes.js.map +1 -0
  194. package/dist/modules/webhooks/elevenlabs-tool.routes.d.ts +20 -0
  195. package/dist/modules/webhooks/elevenlabs-tool.routes.js +85 -0
  196. package/dist/modules/webhooks/elevenlabs-tool.routes.js.map +1 -0
  197. package/dist/modules/webhooks/elevenlabs-tool.service.d.ts +11 -0
  198. package/dist/modules/webhooks/elevenlabs-tool.service.js +360 -0
  199. package/dist/modules/webhooks/elevenlabs-tool.service.js.map +1 -0
  200. package/dist/modules/webhooks/elevenlabs.routes.d.ts +2 -0
  201. package/dist/modules/webhooks/elevenlabs.routes.js +34 -0
  202. package/dist/modules/webhooks/elevenlabs.routes.js.map +1 -0
  203. package/dist/modules/webhooks/elevenlabs.service.d.ts +6 -0
  204. package/dist/modules/webhooks/elevenlabs.service.js +512 -0
  205. package/dist/modules/webhooks/elevenlabs.service.js.map +1 -0
  206. package/dist/modules/webhooks/unipile.routes.d.ts +2 -0
  207. package/dist/modules/webhooks/unipile.routes.js +780 -0
  208. package/dist/modules/webhooks/unipile.routes.js.map +1 -0
  209. package/dist/modules/whatsapp/appointment.model.d.ts +27 -0
  210. package/dist/modules/whatsapp/appointment.model.js +58 -0
  211. package/dist/modules/whatsapp/appointment.model.js.map +1 -0
  212. package/dist/modules/whatsapp/operator-request.model.d.ts +29 -0
  213. package/dist/modules/whatsapp/operator-request.model.js +65 -0
  214. package/dist/modules/whatsapp/operator-request.model.js.map +1 -0
  215. package/dist/modules/whatsapp/stt-usage.model.d.ts +16 -0
  216. package/dist/modules/whatsapp/stt-usage.model.js +53 -0
  217. package/dist/modules/whatsapp/stt-usage.model.js.map +1 -0
  218. package/dist/modules/whatsapp/whatsapp-chat.model.d.ts +23 -0
  219. package/dist/modules/whatsapp/whatsapp-chat.model.js +55 -0
  220. package/dist/modules/whatsapp/whatsapp-chat.model.js.map +1 -0
  221. package/dist/modules/whatsapp/whatsapp-contact-profile.model.d.ts +23 -0
  222. package/dist/modules/whatsapp/whatsapp-contact-profile.model.js +54 -0
  223. package/dist/modules/whatsapp/whatsapp-contact-profile.model.js.map +1 -0
  224. package/dist/modules/whatsapp/whatsapp-message.model.d.ts +30 -0
  225. package/dist/modules/whatsapp/whatsapp-message.model.js +52 -0
  226. package/dist/modules/whatsapp/whatsapp-message.model.js.map +1 -0
  227. package/dist/modules/whatsapp/whatsapp-session.model.d.ts +33 -0
  228. package/dist/modules/whatsapp/whatsapp-session.model.js +65 -0
  229. package/dist/modules/whatsapp/whatsapp-session.model.js.map +1 -0
  230. package/dist/modules/whatsapp/whatsapp.routes.d.ts +2 -0
  231. package/dist/modules/whatsapp/whatsapp.routes.js +1237 -0
  232. package/dist/modules/whatsapp/whatsapp.routes.js.map +1 -0
  233. package/dist/plugins/cors.d.ts +3 -0
  234. package/dist/plugins/cors.js +11 -0
  235. package/dist/plugins/cors.js.map +1 -0
  236. package/dist/plugins/jwt.d.ts +3 -0
  237. package/dist/plugins/jwt.js +22 -0
  238. package/dist/plugins/jwt.js.map +1 -0
  239. package/dist/plugins/rawBody.d.ts +3 -0
  240. package/dist/plugins/rawBody.js +16 -0
  241. package/dist/plugins/rawBody.js.map +1 -0
  242. package/dist/plugins/rbac.d.ts +5 -0
  243. package/dist/plugins/rbac.js +29 -0
  244. package/dist/plugins/rbac.js.map +1 -0
  245. package/dist/routes/admin.routes.d.ts +2 -0
  246. package/dist/routes/admin.routes.js +169 -0
  247. package/dist/routes/admin.routes.js.map +1 -0
  248. package/dist/routes/health.routes.d.ts +2 -0
  249. package/dist/routes/health.routes.js +16 -0
  250. package/dist/routes/health.routes.js.map +1 -0
  251. package/dist/routes/webhook.routes.d.ts +2 -0
  252. package/dist/routes/webhook.routes.js +17 -0
  253. package/dist/routes/webhook.routes.js.map +1 -0
  254. package/dist/services/ai/base.ai.d.ts +19 -0
  255. package/dist/services/ai/base.ai.js +120 -0
  256. package/dist/services/ai/base.ai.js.map +1 -0
  257. package/dist/services/ai/gemini.service.d.ts +11 -0
  258. package/dist/services/ai/gemini.service.js +43 -0
  259. package/dist/services/ai/gemini.service.js.map +1 -0
  260. package/dist/services/ai/index.d.ts +2 -0
  261. package/dist/services/ai/index.js +26 -0
  262. package/dist/services/ai/index.js.map +1 -0
  263. package/dist/services/ai/openai.service.d.ts +11 -0
  264. package/dist/services/ai/openai.service.js +50 -0
  265. package/dist/services/ai/openai.service.js.map +1 -0
  266. package/dist/services/elevenlabs.service.d.ts +52 -0
  267. package/dist/services/elevenlabs.service.js +447 -0
  268. package/dist/services/elevenlabs.service.js.map +1 -0
  269. package/dist/services/google-calendar.service.d.ts +60 -0
  270. package/dist/services/google-calendar.service.js +494 -0
  271. package/dist/services/google-calendar.service.js.map +1 -0
  272. package/dist/services/inbound-call-schedule.service.d.ts +11 -0
  273. package/dist/services/inbound-call-schedule.service.js +162 -0
  274. package/dist/services/inbound-call-schedule.service.js.map +1 -0
  275. package/dist/services/netgsm.service.d.ts +41 -0
  276. package/dist/services/netgsm.service.js +89 -0
  277. package/dist/services/netgsm.service.js.map +1 -0
  278. package/dist/services/unipile.service.d.ts +41 -0
  279. package/dist/services/unipile.service.js +149 -0
  280. package/dist/services/unipile.service.js.map +1 -0
  281. package/dist/services/whatsapp-agent.service.d.ts +139 -0
  282. package/dist/services/whatsapp-agent.service.js +2055 -0
  283. package/dist/services/whatsapp-agent.service.js.map +1 -0
  284. package/dist/services/whatsapp.service.d.ts +26 -0
  285. package/dist/services/whatsapp.service.js +206 -0
  286. package/dist/services/whatsapp.service.js.map +1 -0
  287. package/dist/templates/index.d.ts +39 -0
  288. package/dist/templates/index.js +35 -0
  289. package/dist/templates/index.js.map +1 -0
  290. package/dist/templates/receptionist.d.ts +2 -0
  291. package/dist/templates/receptionist.js +39 -0
  292. package/dist/templates/receptionist.js.map +1 -0
  293. package/dist/templates/survey.d.ts +2 -0
  294. package/dist/templates/survey.js +41 -0
  295. package/dist/templates/survey.js.map +1 -0
  296. package/dist/types/index.d.ts +173 -0
  297. package/dist/types/index.js +3 -0
  298. package/dist/types/index.js.map +1 -0
  299. package/dist/utils/logger.d.ts +3 -0
  300. package/dist/utils/logger.js +105 -0
  301. package/dist/utils/logger.js.map +1 -0
  302. package/docker-compose.nestjs.yml +89 -0
  303. package/docker-compose.yml +78 -0
  304. package/docs/AI_AGENT_ENHANCEMENT_PLAN.md +164 -0
  305. package/docs/API.md +1193 -0
  306. package/docs/API_ENDPOINTS.md +344 -0
  307. package/docs/ARCHITECTURE.md +305 -0
  308. package/docs/AUTH_API.md +252 -0
  309. package/docs/BILLING_SMS_ALERTS.md +94 -0
  310. package/docs/CHAT_ASSIGNMENT_SYSTEM.md +118 -0
  311. package/docs/CLIENT_TOOLS_AND_FEATURES.md +337 -0
  312. package/docs/ELEVENLABS_WEBHOOK_TOOLS.md +644 -0
  313. package/docs/FRONTEND_CHECKLIST.md +227 -0
  314. package/docs/IMPLEMENTATION_STATUS.md +470 -0
  315. package/docs/MIGRATION_GUIDE.md +96 -0
  316. package/docs/MISSINGS_REPORT.md +507 -0
  317. package/docs/NESTJS_MIGRATION_REFERENCE.md +5136 -0
  318. package/docs/PROJECT_DESCRIPTION.md +1038 -0
  319. package/docs/SCALING.md +148 -0
  320. package/docs/SESSION_SUMMARY_2026_03_17.md +135 -0
  321. package/docs/WHATSAPP_AGENT.md +1086 -0
  322. package/docs/architecture/00-SYSTEM-OVERVIEW.md +318 -0
  323. package/docs/architecture/01-DATABASE-SCHEMA.md +2564 -0
  324. package/docs/architecture/02-AUTHENTICATION.md +1040 -0
  325. package/docs/architecture/03-MULTI-CLINIC.md +742 -0
  326. package/docs/architecture/04-WHATSAPP-AGENT.md +608 -0
  327. package/docs/architecture/05-OPERATOR-WORKFLOW.md +444 -0
  328. package/docs/architecture/06-BAILEYS-MICROSERVICE.md +616 -0
  329. package/docs/architecture/07-APPOINTMENTS.md +849 -0
  330. package/docs/architecture/08-VOICE-CALLS.md +470 -0
  331. package/docs/architecture/09-OUTBOUND-CAMPAIGNS.md +542 -0
  332. package/docs/architecture/10-CLIENT-TOOLS.md +665 -0
  333. package/docs/architecture/11-BILLING.md +458 -0
  334. package/docs/architecture/12-SECURITY.md +216 -0
  335. package/docs/architecture/13-LOGGING-AUDIT.md +549 -0
  336. package/docs/architecture/14-META-BUSINESS-API.md +454 -0
  337. package/docs/architecture/15-AI-MODULE.md +479 -0
  338. package/docs/architecture/16-BACKGROUND-JOBS.md +469 -0
  339. package/docs/architecture/17-REALTIME-LIVECHAT.md +447 -0
  340. package/docs/architecture/18-FILE-STORAGE.md +410 -0
  341. package/docs/architecture/19-PATIENTS.md +1034 -0
  342. package/docs/architecture/20-TREATMENTS-AND-PLANS.md +774 -0
  343. package/docs/architecture/21-BEFORE-AFTER-PHOTOS.md +519 -0
  344. package/docs/database.md +456 -0
  345. package/docs/ornek-randevu-onay.csv +3 -0
  346. package/ecosystem.config.js +16 -0
  347. package/elevenlabs-convai-api-reference.md +1171 -0
  348. package/frontend/.dockerignore +2 -0
  349. package/frontend/Dockerfile +24 -0
  350. package/frontend/README.md +75 -0
  351. package/frontend/components.json +25 -0
  352. package/frontend/eslint.config.js +23 -0
  353. package/frontend/index.html +13 -0
  354. package/frontend/nginx.conf +37 -0
  355. package/frontend/package-lock.json +8709 -0
  356. package/frontend/package.json +71 -0
  357. package/frontend/public/favicon.svg +1 -0
  358. package/frontend/public/icons.svg +24 -0
  359. package/frontend/src/App.tsx +125 -0
  360. package/frontend/src/components/error-boundary.tsx +50 -0
  361. package/frontend/src/components/shared/activity-timeline.tsx +66 -0
  362. package/frontend/src/components/shared/appointment-calendar.css +80 -0
  363. package/frontend/src/components/shared/appointment-calendar.tsx +245 -0
  364. package/frontend/src/components/shared/chat-bubble.tsx +72 -0
  365. package/frontend/src/components/shared/combobox.tsx +119 -0
  366. package/frontend/src/components/shared/confirm-dialog.tsx +57 -0
  367. package/frontend/src/components/shared/data-table-pagination.tsx +97 -0
  368. package/frontend/src/components/shared/data-table-toolbar.tsx +39 -0
  369. package/frontend/src/components/shared/empty-state.tsx +27 -0
  370. package/frontend/src/components/shared/file-upload.tsx +140 -0
  371. package/frontend/src/components/shared/index.ts +20 -0
  372. package/frontend/src/components/shared/language-switcher.tsx +29 -0
  373. package/frontend/src/components/shared/notification-dropdown.tsx +115 -0
  374. package/frontend/src/components/shared/page-header.tsx +23 -0
  375. package/frontend/src/components/shared/stat-card.tsx +37 -0
  376. package/frontend/src/components/shared/status-badge.tsx +70 -0
  377. package/frontend/src/components/shared/status-dot.tsx +43 -0
  378. package/frontend/src/components/ui/alert-dialog.tsx +187 -0
  379. package/frontend/src/components/ui/alert.tsx +76 -0
  380. package/frontend/src/components/ui/avatar.tsx +109 -0
  381. package/frontend/src/components/ui/badge.tsx +52 -0
  382. package/frontend/src/components/ui/breadcrumb.tsx +125 -0
  383. package/frontend/src/components/ui/button.tsx +60 -0
  384. package/frontend/src/components/ui/calendar.tsx +219 -0
  385. package/frontend/src/components/ui/card.tsx +103 -0
  386. package/frontend/src/components/ui/chart.tsx +371 -0
  387. package/frontend/src/components/ui/checkbox.tsx +29 -0
  388. package/frontend/src/components/ui/collapsible.tsx +19 -0
  389. package/frontend/src/components/ui/command.tsx +194 -0
  390. package/frontend/src/components/ui/dialog.tsx +158 -0
  391. package/frontend/src/components/ui/dropdown-menu.tsx +268 -0
  392. package/frontend/src/components/ui/input-group.tsx +156 -0
  393. package/frontend/src/components/ui/input.tsx +20 -0
  394. package/frontend/src/components/ui/label.tsx +20 -0
  395. package/frontend/src/components/ui/pagination.tsx +130 -0
  396. package/frontend/src/components/ui/popover.tsx +90 -0
  397. package/frontend/src/components/ui/progress.tsx +83 -0
  398. package/frontend/src/components/ui/radio-group.tsx +36 -0
  399. package/frontend/src/components/ui/scroll-area.tsx +54 -0
  400. package/frontend/src/components/ui/select.tsx +199 -0
  401. package/frontend/src/components/ui/separator.tsx +23 -0
  402. package/frontend/src/components/ui/sheet.tsx +138 -0
  403. package/frontend/src/components/ui/sidebar.tsx +723 -0
  404. package/frontend/src/components/ui/skeleton.tsx +13 -0
  405. package/frontend/src/components/ui/sonner.tsx +47 -0
  406. package/frontend/src/components/ui/switch.tsx +30 -0
  407. package/frontend/src/components/ui/table.tsx +114 -0
  408. package/frontend/src/components/ui/tabs.tsx +82 -0
  409. package/frontend/src/components/ui/textarea.tsx +18 -0
  410. package/frontend/src/components/ui/toggle-group.tsx +89 -0
  411. package/frontend/src/components/ui/toggle.tsx +43 -0
  412. package/frontend/src/components/ui/tooltip.tsx +64 -0
  413. package/frontend/src/hooks/use-mobile.ts +19 -0
  414. package/frontend/src/i18n/index.ts +20 -0
  415. package/frontend/src/i18n/locales/en.json +786 -0
  416. package/frontend/src/i18n/locales/tr.json +786 -0
  417. package/frontend/src/index.css +134 -0
  418. package/frontend/src/layouts/admin-layout.tsx +111 -0
  419. package/frontend/src/layouts/app-header.tsx +130 -0
  420. package/frontend/src/layouts/app-layout.tsx +19 -0
  421. package/frontend/src/layouts/app-sidebar.tsx +212 -0
  422. package/frontend/src/layouts/auth-guard.tsx +31 -0
  423. package/frontend/src/layouts/mobile-sidebar.tsx +120 -0
  424. package/frontend/src/lib/api.ts +68 -0
  425. package/frontend/src/lib/hooks/use-appointments.ts +224 -0
  426. package/frontend/src/lib/hooks/use-availability-blocks.ts +83 -0
  427. package/frontend/src/lib/hooks/use-chats.ts +236 -0
  428. package/frontend/src/lib/hooks/use-clinic-detail.ts +171 -0
  429. package/frontend/src/lib/hooks/use-clinics.ts +37 -0
  430. package/frontend/src/lib/hooks/use-doctor-calendar.ts +80 -0
  431. package/frontend/src/lib/hooks/use-examinations.ts +89 -0
  432. package/frontend/src/lib/hooks/use-medical-history.ts +52 -0
  433. package/frontend/src/lib/hooks/use-patients.ts +296 -0
  434. package/frontend/src/lib/hooks/use-photo-sets.ts +118 -0
  435. package/frontend/src/lib/hooks/use-treatment-plans.ts +152 -0
  436. package/frontend/src/lib/hooks/use-treatments.ts +125 -0
  437. package/frontend/src/lib/hooks/use-users.ts +172 -0
  438. package/frontend/src/lib/utils.ts +6 -0
  439. package/frontend/src/main.tsx +17 -0
  440. package/frontend/src/pages/admin/agents/detail.tsx +774 -0
  441. package/frontend/src/pages/admin/agents/import.tsx +280 -0
  442. package/frontend/src/pages/admin/agents/index.tsx +245 -0
  443. package/frontend/src/pages/admin/ai-playground.tsx +543 -0
  444. package/frontend/src/pages/appointments/create-appointment-dialog.tsx +390 -0
  445. package/frontend/src/pages/appointments/index.tsx +860 -0
  446. package/frontend/src/pages/audit/index.tsx +24 -0
  447. package/frontend/src/pages/auth/login.tsx +194 -0
  448. package/frontend/src/pages/billing/index.tsx +24 -0
  449. package/frontend/src/pages/calendar/index.tsx +704 -0
  450. package/frontend/src/pages/calendar-connections/index.tsx +295 -0
  451. package/frontend/src/pages/calls/index.tsx +24 -0
  452. package/frontend/src/pages/campaigns/index.tsx +24 -0
  453. package/frontend/src/pages/chats/index.tsx +981 -0
  454. package/frontend/src/pages/clinics/index.tsx +224 -0
  455. package/frontend/src/pages/clinics/settings.tsx +412 -0
  456. package/frontend/src/pages/components-showcase.tsx +773 -0
  457. package/frontend/src/pages/connections/index.tsx +328 -0
  458. package/frontend/src/pages/dashboard.tsx +50 -0
  459. package/frontend/src/pages/leads/index.tsx +24 -0
  460. package/frontend/src/pages/my-schedule/index.tsx +496 -0
  461. package/frontend/src/pages/patients/create-patient-dialog.tsx +358 -0
  462. package/frontend/src/pages/patients/detail.tsx +1195 -0
  463. package/frontend/src/pages/patients/edit-patient-dialog.tsx +387 -0
  464. package/frontend/src/pages/patients/examinations-tab.tsx +460 -0
  465. package/frontend/src/pages/patients/index.tsx +381 -0
  466. package/frontend/src/pages/patients/medical-history-dialog.tsx +207 -0
  467. package/frontend/src/pages/patients/photo-sets-tab.tsx +616 -0
  468. package/frontend/src/pages/patients/timeline-tab.tsx +164 -0
  469. package/frontend/src/pages/patients/treatment-plans-tab.tsx +598 -0
  470. package/frontend/src/pages/phone-numbers/detail.tsx +427 -0
  471. package/frontend/src/pages/phone-numbers/index.tsx +455 -0
  472. package/frontend/src/pages/platform/index.tsx +454 -0
  473. package/frontend/src/pages/settings/index.tsx +126 -0
  474. package/frontend/src/pages/treatments/index.tsx +487 -0
  475. package/frontend/src/pages/users/doctor-profile.tsx +672 -0
  476. package/frontend/src/pages/users/edit.tsx +329 -0
  477. package/frontend/src/pages/users/index.tsx +407 -0
  478. package/frontend/src/pages/validation/index.tsx +24 -0
  479. package/frontend/src/stores/auth.store.ts +108 -0
  480. package/frontend/src/stores/ui.store.ts +41 -0
  481. package/frontend/tsconfig.app.json +32 -0
  482. package/frontend/tsconfig.json +13 -0
  483. package/frontend/tsconfig.node.json +26 -0
  484. package/frontend/vite.config.ts +29 -0
  485. package/nestjs/.dockerignore +5 -0
  486. package/nestjs/.env.docker +64 -0
  487. package/nestjs/.prettierrc +4 -0
  488. package/nestjs/Dockerfile +36 -0
  489. package/nestjs/README.md +98 -0
  490. package/nestjs/eslint.config.mjs +35 -0
  491. package/nestjs/nest-cli.json +8 -0
  492. package/nestjs/package-lock.json +13390 -0
  493. package/nestjs/package.json +114 -0
  494. package/nestjs/prisma/migrations/20260409161536_add_message_metadata_fields/migration.sql +1746 -0
  495. package/nestjs/prisma/migrations/20260410140436_add_agent_ai_fields/migration.sql +36 -0
  496. package/nestjs/prisma/migrations/20260410175519_add_agent_clinic_assignments/migration.sql +21 -0
  497. package/nestjs/prisma/migrations/20260412094344_make_agent_tenant_optional/migration.sql +2 -0
  498. package/nestjs/prisma/migrations/20260412110008_add_admin_chat_sessions/migration.sql +47 -0
  499. package/nestjs/prisma/migrations/migration_lock.toml +3 -0
  500. package/nestjs/prisma/schema.prisma +1843 -0
  501. package/nestjs/prisma/seed.ts +375 -0
  502. package/nestjs/prisma.config.ts +14 -0
  503. package/nestjs/src/admin/admin.controller.ts +27 -0
  504. package/nestjs/src/admin/admin.module.ts +16 -0
  505. package/nestjs/src/admin/admin.service.ts +91 -0
  506. package/nestjs/src/admin/ai-chat-session.service.ts +454 -0
  507. package/nestjs/src/admin/ai-test.controller.ts +191 -0
  508. package/nestjs/src/admin/superadmin.controller.ts +106 -0
  509. package/nestjs/src/agents/agents.controller.ts +262 -0
  510. package/nestjs/src/agents/agents.module.ts +13 -0
  511. package/nestjs/src/agents/agents.service.ts +733 -0
  512. package/nestjs/src/agents/dto/create-agent.dto.ts +99 -0
  513. package/nestjs/src/agents/dto/index.ts +2 -0
  514. package/nestjs/src/agents/dto/update-agent.dto.ts +148 -0
  515. package/nestjs/src/app.module.ts +115 -0
  516. package/nestjs/src/appointment-validation/appointment-validation.controller.ts +194 -0
  517. package/nestjs/src/appointment-validation/appointment-validation.module.ts +16 -0
  518. package/nestjs/src/appointment-validation/appointment-validation.service.ts +450 -0
  519. package/nestjs/src/appointment-validation/dto/create-batch.dto.ts +105 -0
  520. package/nestjs/src/appointment-validation/dto/index.ts +1 -0
  521. package/nestjs/src/appointment-validation/processors/validation-dispatch.processor.ts +26 -0
  522. package/nestjs/src/appointment-validation/processors/validation-sync.processor.ts +23 -0
  523. package/nestjs/src/appointments/appointments.controller.ts +268 -0
  524. package/nestjs/src/appointments/appointments.module.ts +13 -0
  525. package/nestjs/src/appointments/appointments.service.ts +773 -0
  526. package/nestjs/src/appointments/dto/create-appointment.dto.ts +72 -0
  527. package/nestjs/src/appointments/dto/index.ts +4 -0
  528. package/nestjs/src/appointments/dto/query-appointments.dto.ts +60 -0
  529. package/nestjs/src/appointments/dto/update-appointment.dto.ts +43 -0
  530. package/nestjs/src/appointments/dto/update-status.dto.ts +18 -0
  531. package/nestjs/src/appointments/processors/reminder.processor.ts +243 -0
  532. package/nestjs/src/audit/audit.controller.ts +84 -0
  533. package/nestjs/src/audit/audit.decorator.ts +20 -0
  534. package/nestjs/src/audit/audit.interceptor.ts +67 -0
  535. package/nestjs/src/audit/audit.interfaces.ts +15 -0
  536. package/nestjs/src/audit/audit.module.ts +12 -0
  537. package/nestjs/src/audit/audit.service.ts +177 -0
  538. package/nestjs/src/auth/auth.controller.ts +116 -0
  539. package/nestjs/src/auth/auth.module.ts +25 -0
  540. package/nestjs/src/auth/auth.service.ts +612 -0
  541. package/nestjs/src/auth/dto/change-password.dto.ts +13 -0
  542. package/nestjs/src/auth/dto/forgot-password.dto.ts +8 -0
  543. package/nestjs/src/auth/dto/index.ts +6 -0
  544. package/nestjs/src/auth/dto/login.dto.ts +13 -0
  545. package/nestjs/src/auth/dto/refresh.dto.ts +8 -0
  546. package/nestjs/src/auth/dto/register.dto.ts +28 -0
  547. package/nestjs/src/auth/dto/reset-password.dto.ts +13 -0
  548. package/nestjs/src/auth/permissions.config.ts +85 -0
  549. package/nestjs/src/availability-blocks/availability-blocks.controller.ts +83 -0
  550. package/nestjs/src/availability-blocks/availability-blocks.module.ts +10 -0
  551. package/nestjs/src/availability-blocks/availability-blocks.service.ts +202 -0
  552. package/nestjs/src/billing/billing.controller.ts +104 -0
  553. package/nestjs/src/billing/billing.module.ts +12 -0
  554. package/nestjs/src/billing/billing.service.ts +398 -0
  555. package/nestjs/src/billing/dto/index.ts +2 -0
  556. package/nestjs/src/billing/dto/query-billing.dto.ts +29 -0
  557. package/nestjs/src/billing/dto/record-payment.dto.ts +60 -0
  558. package/nestjs/src/billing/processors/alerts.processor.ts +216 -0
  559. package/nestjs/src/billing/processors/snapshot.processor.ts +181 -0
  560. package/nestjs/src/calls/calls.controller.ts +92 -0
  561. package/nestjs/src/calls/calls.module.ts +10 -0
  562. package/nestjs/src/calls/calls.service.ts +359 -0
  563. package/nestjs/src/calls/dto/index.ts +1 -0
  564. package/nestjs/src/calls/dto/query-calls.dto.ts +46 -0
  565. package/nestjs/src/clinics/clinics.controller.ts +226 -0
  566. package/nestjs/src/clinics/clinics.module.ts +11 -0
  567. package/nestjs/src/clinics/clinics.service.ts +203 -0
  568. package/nestjs/src/clinics/dto/create-clinic.dto.ts +40 -0
  569. package/nestjs/src/clinics/dto/create-meta-connection.dto.ts +44 -0
  570. package/nestjs/src/clinics/dto/index.ts +4 -0
  571. package/nestjs/src/clinics/dto/update-clinic.dto.ts +133 -0
  572. package/nestjs/src/clinics/dto/update-meta-connection.dto.ts +22 -0
  573. package/nestjs/src/clinics/meta-connections.service.ts +210 -0
  574. package/nestjs/src/common/decorators/accessible-clinics.decorator.ts +9 -0
  575. package/nestjs/src/common/decorators/current-user.decorator.ts +10 -0
  576. package/nestjs/src/common/decorators/features.decorator.ts +7 -0
  577. package/nestjs/src/common/decorators/index.ts +4 -0
  578. package/nestjs/src/common/decorators/permissions.decorator.ts +13 -0
  579. package/nestjs/src/common/gateways/events.gateway.ts +157 -0
  580. package/nestjs/src/common/guards/clinic-access.guard.ts +40 -0
  581. package/nestjs/src/common/guards/features.guard.ts +38 -0
  582. package/nestjs/src/common/guards/index.ts +5 -0
  583. package/nestjs/src/common/guards/jwt-auth.guard.ts +54 -0
  584. package/nestjs/src/common/guards/permissions.guard.ts +50 -0
  585. package/nestjs/src/common/guards/superadmin.guard.ts +35 -0
  586. package/nestjs/src/common/interceptors/request-context.interceptor.ts +32 -0
  587. package/nestjs/src/common/interceptors/superadmin-tenant.interceptor.ts +42 -0
  588. package/nestjs/src/common/interfaces/jwt-payload.interface.ts +11 -0
  589. package/nestjs/src/config/config.module.ts +13 -0
  590. package/nestjs/src/database/database.module.ts +9 -0
  591. package/nestjs/src/database/prisma.service.ts +20 -0
  592. package/nestjs/src/database/tenant-context.ts +27 -0
  593. package/nestjs/src/google-calendar/google-calendar.controller.ts +259 -0
  594. package/nestjs/src/google-calendar/google-calendar.module.ts +10 -0
  595. package/nestjs/src/google-calendar/google-calendar.service.ts +811 -0
  596. package/nestjs/src/integrations/ai/ai.interface.ts +74 -0
  597. package/nestjs/src/integrations/ai/ai.module.ts +11 -0
  598. package/nestjs/src/integrations/ai/ai.service.ts +148 -0
  599. package/nestjs/src/integrations/ai/providers/anthropic.provider.ts +146 -0
  600. package/nestjs/src/integrations/ai/providers/openai.provider.ts +158 -0
  601. package/nestjs/src/integrations/elevenlabs/elevenlabs.module.ts +8 -0
  602. package/nestjs/src/integrations/elevenlabs/elevenlabs.service.ts +226 -0
  603. package/nestjs/src/integrations/encryption/encryption.module.ts +9 -0
  604. package/nestjs/src/integrations/encryption/encryption.service.ts +31 -0
  605. package/nestjs/src/integrations/image/image.module.ts +9 -0
  606. package/nestjs/src/integrations/image/image.service.ts +61 -0
  607. package/nestjs/src/integrations/meta-business/meta-business.module.ts +10 -0
  608. package/nestjs/src/integrations/meta-business/meta-instagram.service.ts +94 -0
  609. package/nestjs/src/integrations/meta-business/meta-webhook.service.ts +52 -0
  610. package/nestjs/src/integrations/meta-business/meta-whatsapp.service.ts +254 -0
  611. package/nestjs/src/integrations/minio/minio.module.ts +9 -0
  612. package/nestjs/src/integrations/minio/minio.service.ts +88 -0
  613. package/nestjs/src/integrations/netgsm/netgsm.module.ts +8 -0
  614. package/nestjs/src/integrations/netgsm/netgsm.service.ts +17 -0
  615. package/nestjs/src/integrations/tektippay/tektippay.module.ts +8 -0
  616. package/nestjs/src/integrations/tektippay/tektippay.service.ts +23 -0
  617. package/nestjs/src/leads/dto/index.ts +2 -0
  618. package/nestjs/src/leads/dto/query-leads.dto.ts +50 -0
  619. package/nestjs/src/leads/dto/update-lead.dto.ts +26 -0
  620. package/nestjs/src/leads/leads.controller.ts +184 -0
  621. package/nestjs/src/leads/leads.module.ts +10 -0
  622. package/nestjs/src/leads/leads.service.ts +375 -0
  623. package/nestjs/src/logging/logging.controller.ts +82 -0
  624. package/nestjs/src/logging/logging.module.ts +11 -0
  625. package/nestjs/src/logging/logging.service.ts +180 -0
  626. package/nestjs/src/main.ts +86 -0
  627. package/nestjs/src/outbound-campaigns/dto/create-campaign.dto.ts +47 -0
  628. package/nestjs/src/outbound-campaigns/dto/index.ts +3 -0
  629. package/nestjs/src/outbound-campaigns/dto/update-campaign.dto.ts +31 -0
  630. package/nestjs/src/outbound-campaigns/dto/upload-entries.dto.ts +35 -0
  631. package/nestjs/src/outbound-campaigns/outbound-campaigns.controller.ts +307 -0
  632. package/nestjs/src/outbound-campaigns/outbound-campaigns.module.ts +13 -0
  633. package/nestjs/src/outbound-campaigns/outbound-campaigns.service.ts +471 -0
  634. package/nestjs/src/outbound-campaigns/processors/campaign-dispatch.processor.ts +366 -0
  635. package/nestjs/src/patients/documents.service.ts +231 -0
  636. package/nestjs/src/patients/dto/create-examination.dto.ts +34 -0
  637. package/nestjs/src/patients/dto/create-note.dto.ts +14 -0
  638. package/nestjs/src/patients/dto/create-patient.dto.ts +86 -0
  639. package/nestjs/src/patients/dto/create-photo-set.dto.ts +32 -0
  640. package/nestjs/src/patients/dto/index.ts +10 -0
  641. package/nestjs/src/patients/dto/update-examination.dto.ts +29 -0
  642. package/nestjs/src/patients/dto/update-medical-history.dto.ts +47 -0
  643. package/nestjs/src/patients/dto/update-patient.dto.ts +87 -0
  644. package/nestjs/src/patients/dto/update-photo-set.dto.ts +28 -0
  645. package/nestjs/src/patients/dto/upload-document.dto.ts +31 -0
  646. package/nestjs/src/patients/dto/upload-photo.dto.ts +27 -0
  647. package/nestjs/src/patients/examinations.service.ts +271 -0
  648. package/nestjs/src/patients/medical-history.service.ts +149 -0
  649. package/nestjs/src/patients/notes.service.ts +172 -0
  650. package/nestjs/src/patients/patients.controller.ts +485 -0
  651. package/nestjs/src/patients/patients.module.ts +22 -0
  652. package/nestjs/src/patients/patients.service.ts +412 -0
  653. package/nestjs/src/patients/photo-sets.service.ts +389 -0
  654. package/nestjs/src/phone-numbers/dto/create-phone-number.dto.ts +57 -0
  655. package/nestjs/src/phone-numbers/dto/index.ts +3 -0
  656. package/nestjs/src/phone-numbers/dto/update-phone-number.dto.ts +38 -0
  657. package/nestjs/src/phone-numbers/dto/update-schedule.dto.ts +39 -0
  658. package/nestjs/src/phone-numbers/phone-numbers.controller.ts +125 -0
  659. package/nestjs/src/phone-numbers/phone-numbers.module.ts +10 -0
  660. package/nestjs/src/phone-numbers/phone-numbers.service.ts +209 -0
  661. package/nestjs/src/plans/dto/create-plan.dto.ts +70 -0
  662. package/nestjs/src/plans/dto/index.ts +2 -0
  663. package/nestjs/src/plans/dto/update-plan.dto.ts +80 -0
  664. package/nestjs/src/plans/plans.controller.ts +50 -0
  665. package/nestjs/src/plans/plans.module.ts +10 -0
  666. package/nestjs/src/plans/plans.service.ts +115 -0
  667. package/nestjs/src/platform/dto/create-tenant.dto.ts +36 -0
  668. package/nestjs/src/platform/dto/index.ts +2 -0
  669. package/nestjs/src/platform/dto/update-tenant-platform.dto.ts +44 -0
  670. package/nestjs/src/platform/platform.controller.ts +79 -0
  671. package/nestjs/src/platform/platform.module.ts +10 -0
  672. package/nestjs/src/platform/platform.service.ts +301 -0
  673. package/nestjs/src/queue/queue.module.ts +56 -0
  674. package/nestjs/src/redis/redis.module.ts +20 -0
  675. package/nestjs/src/tenants/dto/index.ts +1 -0
  676. package/nestjs/src/tenants/dto/update-tenant.dto.ts +15 -0
  677. package/nestjs/src/tenants/tenants.controller.ts +45 -0
  678. package/nestjs/src/tenants/tenants.module.ts +10 -0
  679. package/nestjs/src/tenants/tenants.service.ts +41 -0
  680. package/nestjs/src/tools/adapters/elevenlabs-http.adapter.ts +51 -0
  681. package/nestjs/src/tools/adapters/elevenlabs-ws.adapter.ts +59 -0
  682. package/nestjs/src/tools/handlers/calendar.tools.ts +441 -0
  683. package/nestjs/src/tools/handlers/notification.tools.ts +34 -0
  684. package/nestjs/src/tools/handlers/operator.tools.ts +303 -0
  685. package/nestjs/src/tools/handlers/patient.tools.ts +242 -0
  686. package/nestjs/src/tools/handlers/payment.tools.ts +43 -0
  687. package/nestjs/src/tools/handlers/validation.tools.ts +152 -0
  688. package/nestjs/src/tools/tool-registry.service.ts +221 -0
  689. package/nestjs/src/tools/tool.decorator.ts +16 -0
  690. package/nestjs/src/tools/tool.interfaces.ts +26 -0
  691. package/nestjs/src/tools/tools.module.ts +50 -0
  692. package/nestjs/src/treatments/dto/create-plan-item.dto.ts +27 -0
  693. package/nestjs/src/treatments/dto/create-treatment-plan.dto.ts +69 -0
  694. package/nestjs/src/treatments/dto/create-treatment.dto.ts +59 -0
  695. package/nestjs/src/treatments/dto/index.ts +6 -0
  696. package/nestjs/src/treatments/dto/update-plan-item.dto.ts +23 -0
  697. package/nestjs/src/treatments/dto/update-treatment-plan.dto.ts +22 -0
  698. package/nestjs/src/treatments/dto/update-treatment.dto.ts +70 -0
  699. package/nestjs/src/treatments/treatment-plans.service.ts +362 -0
  700. package/nestjs/src/treatments/treatments.controller.ts +265 -0
  701. package/nestjs/src/treatments/treatments.module.ts +14 -0
  702. package/nestjs/src/treatments/treatments.service.ts +165 -0
  703. package/nestjs/src/users/doctor-profiles.service.ts +202 -0
  704. package/nestjs/src/users/dto/index.ts +4 -0
  705. package/nestjs/src/users/dto/invite-user.dto.ts +52 -0
  706. package/nestjs/src/users/dto/update-clinic-assignments.dto.ts +9 -0
  707. package/nestjs/src/users/dto/update-doctor-profile.dto.ts +49 -0
  708. package/nestjs/src/users/dto/update-user.dto.ts +41 -0
  709. package/nestjs/src/users/users.controller.ts +142 -0
  710. package/nestjs/src/users/users.module.ts +11 -0
  711. package/nestjs/src/users/users.service.ts +250 -0
  712. package/nestjs/src/webhooks/elevenlabs-tool.controller.ts +66 -0
  713. package/nestjs/src/webhooks/elevenlabs-webhook.controller.ts +60 -0
  714. package/nestjs/src/webhooks/meta-webhook.controller.ts +178 -0
  715. package/nestjs/src/webhooks/processors/elevenlabs-webhook.processor.ts +28 -0
  716. package/nestjs/src/webhooks/webhooks.module.ts +17 -0
  717. package/nestjs/src/whatsapp/chat-context.service.ts +281 -0
  718. package/nestjs/src/whatsapp/dto/add-blacklist.dto.ts +13 -0
  719. package/nestjs/src/whatsapp/dto/index.ts +2 -0
  720. package/nestjs/src/whatsapp/dto/send-message.dto.ts +14 -0
  721. package/nestjs/src/whatsapp/listeners/meta-message.listener.ts +579 -0
  722. package/nestjs/src/whatsapp/meta-auth.controller.ts +268 -0
  723. package/nestjs/src/whatsapp/meta-instagram-auth.controller.ts +244 -0
  724. package/nestjs/src/whatsapp/operator.service.ts +692 -0
  725. package/nestjs/src/whatsapp/processors/cost-sweep.processor.ts +130 -0
  726. package/nestjs/src/whatsapp/processors/grace.processor.ts +191 -0
  727. package/nestjs/src/whatsapp/processors/message-buffer.processor.ts +138 -0
  728. package/nestjs/src/whatsapp/processors/message-cleanup.processor.ts +148 -0
  729. package/nestjs/src/whatsapp/processors/meta-token-refresh.processor.ts +114 -0
  730. package/nestjs/src/whatsapp/processors/operator-expiry.processor.ts +105 -0
  731. package/nestjs/src/whatsapp/processors/profile-update.processor.ts +234 -0
  732. package/nestjs/src/whatsapp/processors/session-cleanup.processor.ts +178 -0
  733. package/nestjs/src/whatsapp/processors/session-labels.processor.ts +248 -0
  734. package/nestjs/src/whatsapp/whatsapp-agent.service.ts +2506 -0
  735. package/nestjs/src/whatsapp/whatsapp-recovery.service.ts +117 -0
  736. package/nestjs/src/whatsapp/whatsapp.controller.ts +398 -0
  737. package/nestjs/src/whatsapp/whatsapp.module.ts +51 -0
  738. package/nestjs/src/whatsapp/whatsapp.service.ts +592 -0
  739. package/nestjs/test/app.e2e-spec.ts +25 -0
  740. package/nestjs/test/jest-e2e.json +9 -0
  741. package/nestjs/tsconfig.build.json +4 -0
  742. package/nestjs/tsconfig.json +25 -0
  743. package/nginx.example.conf +18 -0
  744. package/package.json +47 -0
  745. package/scripts/addRecipient.ts +48 -0
  746. package/scripts/listRecipients.ts +31 -0
  747. package/scripts/migrate-agent-ref.ts +86 -0
  748. package/scripts/migrate-sessions.ts +183 -0
  749. package/scripts/promote.ts +27 -0
  750. package/scripts/retrigger.ts +84 -0
  751. package/scripts/seed.ts +435 -0
  752. package/scripts/testSend.ts +63 -0
  753. package/src/app.ts +85 -0
  754. package/src/config/agentPrompts.json +19 -0
  755. package/src/config/database.ts +21 -0
  756. package/src/config/env.ts +94 -0
  757. package/src/config/index.ts +86 -0
  758. package/src/controllers/webhook.controller.ts +150 -0
  759. package/src/errors/index.ts +9 -0
  760. package/src/hooks/webhookValidator.ts +55 -0
  761. package/src/index.ts +68 -0
  762. package/src/migrations/001_session-architecture.ts +138 -0
  763. package/src/migrations/002_agent-ref.ts +65 -0
  764. package/src/migrations/003_shift-schedule.ts +55 -0
  765. package/src/migrations/004_invert-shift-to-clinic-hours.ts +30 -0
  766. package/src/migrations/005_composite-baileys-chat-ids.ts +60 -0
  767. package/src/migrations/migration.model.ts +27 -0
  768. package/src/migrations/migration.routes.ts +40 -0
  769. package/src/migrations/migration.runner.ts +112 -0
  770. package/src/models/ConversationClaim.ts +17 -0
  771. package/src/models/ConversationEvaluation.ts +91 -0
  772. package/src/models/Summary.ts +77 -0
  773. package/src/models/Transcription.ts +68 -0
  774. package/src/models/WhatsAppRecipient.ts +37 -0
  775. package/src/modules/admin/admin.routes.ts +2385 -0
  776. package/src/modules/admin/elevenlabs-test-chat.routes.ts +193 -0
  777. package/src/modules/admin/whatsapp-test-chat.routes.ts +244 -0
  778. package/src/modules/agents/agent.model.ts +93 -0
  779. package/src/modules/agents/agent.routes.ts +65 -0
  780. package/src/modules/appointment-validation/appointment-validation.model.ts +163 -0
  781. package/src/modules/appointment-validation/appointment-validation.routes.ts +275 -0
  782. package/src/modules/appointment-validation/appointment-validation.service.ts +1028 -0
  783. package/src/modules/auth/auth.model.ts +42 -0
  784. package/src/modules/auth/auth.routes.ts +199 -0
  785. package/src/modules/auth/auth.service.ts +210 -0
  786. package/src/modules/auth/refresh-token.model.ts +26 -0
  787. package/src/modules/billing/billing-alert.model.ts +28 -0
  788. package/src/modules/billing/billing-period-snapshot.model.ts +68 -0
  789. package/src/modules/billing/billing.model.ts +42 -0
  790. package/src/modules/billing/billing.routes.ts +67 -0
  791. package/src/modules/billing/billing.service.ts +562 -0
  792. package/src/modules/billing/payment.model.ts +42 -0
  793. package/src/modules/calls/call.model.ts +102 -0
  794. package/src/modules/calls/call.routes.ts +118 -0
  795. package/src/modules/campaigns/campaign.model.ts +111 -0
  796. package/src/modules/campaigns/campaign.routes.ts +402 -0
  797. package/src/modules/campaigns/campaign.service.ts +99 -0
  798. package/src/modules/google-calendar/google-calendar.routes.ts +31 -0
  799. package/src/modules/inbound-call/inbound-call-config.model.ts +49 -0
  800. package/src/modules/inbound-call/inbound-call.routes.ts +289 -0
  801. package/src/modules/leads/lead.model.ts +40 -0
  802. package/src/modules/leads/lead.routes.ts +246 -0
  803. package/src/modules/logs/log.model.ts +27 -0
  804. package/src/modules/logs/log.routes.ts +102 -0
  805. package/src/modules/plans/plan.model.ts +45 -0
  806. package/src/modules/surveys/survey.model.ts +70 -0
  807. package/src/modules/surveys/survey.routes.ts +187 -0
  808. package/src/modules/tenants/tenant.model.ts +181 -0
  809. package/src/modules/tenants/tenant.routes.ts +78 -0
  810. package/src/modules/users/user.routes.ts +126 -0
  811. package/src/modules/webhooks/elevenlabs-tool.routes.ts +94 -0
  812. package/src/modules/webhooks/elevenlabs-tool.service.ts +491 -0
  813. package/src/modules/webhooks/elevenlabs.routes.ts +34 -0
  814. package/src/modules/webhooks/elevenlabs.service.ts +565 -0
  815. package/src/modules/webhooks/unipile.routes.ts +917 -0
  816. package/src/modules/whatsapp/appointment.model.ts +47 -0
  817. package/src/modules/whatsapp/operator-request.model.ts +58 -0
  818. package/src/modules/whatsapp/stt-usage.model.ts +30 -0
  819. package/src/modules/whatsapp/whatsapp-chat.model.ts +39 -0
  820. package/src/modules/whatsapp/whatsapp-contact-profile.model.ts +41 -0
  821. package/src/modules/whatsapp/whatsapp-message.model.ts +41 -0
  822. package/src/modules/whatsapp/whatsapp-session.model.ts +60 -0
  823. package/src/modules/whatsapp/whatsapp.routes.ts +1435 -0
  824. package/src/plugins/cors.ts +7 -0
  825. package/src/plugins/jwt.ts +18 -0
  826. package/src/plugins/rawBody.ts +12 -0
  827. package/src/plugins/rbac.ts +24 -0
  828. package/src/routes/admin.routes.ts +208 -0
  829. package/src/routes/health.routes.ts +12 -0
  830. package/src/routes/webhook.routes.ts +12 -0
  831. package/src/services/ai/base.ai.ts +132 -0
  832. package/src/services/ai/gemini.service.ts +41 -0
  833. package/src/services/ai/index.ts +24 -0
  834. package/src/services/ai/openai.service.ts +48 -0
  835. package/src/services/elevenlabs.service.ts +532 -0
  836. package/src/services/google-calendar.service.ts +656 -0
  837. package/src/services/inbound-call-schedule.service.ts +174 -0
  838. package/src/services/netgsm.service.ts +128 -0
  839. package/src/services/unipile.service.ts +200 -0
  840. package/src/services/whatsapp-agent.service.ts +2479 -0
  841. package/src/services/whatsapp.service.ts +245 -0
  842. package/src/templates/index.ts +71 -0
  843. package/src/templates/receptionist.ts +44 -0
  844. package/src/templates/survey.ts +44 -0
  845. package/src/types/index.ts +218 -0
  846. package/src/utils/logger.ts +83 -0
  847. package/tsconfig.json +19 -0
  848. package/web/.dockerignore +4 -0
  849. package/web/Dockerfile +18 -0
  850. package/web/README.md +73 -0
  851. package/web/components.json +23 -0
  852. package/web/eslint.config.js +23 -0
  853. package/web/index.html +14 -0
  854. package/web/nginx.conf +18 -0
  855. package/web/package-lock.json +10292 -0
  856. package/web/package.json +48 -0
  857. package/web/public/favicon.ico +0 -0
  858. package/web/public/vite.svg +1 -0
  859. package/web/src/App.tsx +191 -0
  860. package/web/src/assets/react.svg +1 -0
  861. package/web/src/components/Layout.tsx +261 -0
  862. package/web/src/components/LeadConversation.tsx +251 -0
  863. package/web/src/components/ProtectedRoute.tsx +20 -0
  864. package/web/src/components/conversation-review/CardAudioPlayer.tsx +200 -0
  865. package/web/src/components/conversation-review/CardEvaluation.tsx +351 -0
  866. package/web/src/components/conversation-review/CardMetadata.tsx +44 -0
  867. package/web/src/components/conversation-review/ConversationCard.tsx +95 -0
  868. package/web/src/components/conversation-review/ReviewContainer.tsx +120 -0
  869. package/web/src/components/conversation-review/ReviewFilters.tsx +88 -0
  870. package/web/src/components/empty-state.tsx +15 -0
  871. package/web/src/components/page-header.tsx +19 -0
  872. package/web/src/components/page-loader.tsx +32 -0
  873. package/web/src/components/pagination.tsx +38 -0
  874. package/web/src/components/stat-card.tsx +27 -0
  875. package/web/src/components/status-badge.tsx +125 -0
  876. package/web/src/components/ui/alert.tsx +66 -0
  877. package/web/src/components/ui/badge.tsx +48 -0
  878. package/web/src/components/ui/button.tsx +64 -0
  879. package/web/src/components/ui/card.tsx +92 -0
  880. package/web/src/components/ui/checkbox.tsx +32 -0
  881. package/web/src/components/ui/dialog.tsx +158 -0
  882. package/web/src/components/ui/dropdown-menu.tsx +255 -0
  883. package/web/src/components/ui/input.tsx +21 -0
  884. package/web/src/components/ui/label.tsx +22 -0
  885. package/web/src/components/ui/progress.tsx +29 -0
  886. package/web/src/components/ui/select.tsx +188 -0
  887. package/web/src/components/ui/separator.tsx +28 -0
  888. package/web/src/components/ui/sheet.tsx +123 -0
  889. package/web/src/components/ui/skeleton.tsx +13 -0
  890. package/web/src/components/ui/sonner.tsx +35 -0
  891. package/web/src/components/ui/table.tsx +116 -0
  892. package/web/src/components/ui/tabs.tsx +89 -0
  893. package/web/src/components/ui/textarea.tsx +18 -0
  894. package/web/src/components/ui/tooltip.tsx +57 -0
  895. package/web/src/components/whatsapp/ChatDetail.tsx +417 -0
  896. package/web/src/components/whatsapp/ChatHeader.tsx +78 -0
  897. package/web/src/components/whatsapp/ChatList.tsx +107 -0
  898. package/web/src/components/whatsapp/ChatListItem.tsx +60 -0
  899. package/web/src/components/whatsapp/MessageBubble.tsx +46 -0
  900. package/web/src/components/whatsapp/MessageInput.tsx +63 -0
  901. package/web/src/components/whatsapp/MessageStream.tsx +135 -0
  902. package/web/src/components/whatsapp/SessionDivider.tsx +65 -0
  903. package/web/src/components/whatsapp/SessionHistory.tsx +119 -0
  904. package/web/src/components/whatsapp/settings/AiSettingsTab.tsx +268 -0
  905. package/web/src/components/whatsapp/settings/CalendarTab.tsx +339 -0
  906. package/web/src/components/whatsapp/settings/ConnectionTab.tsx +236 -0
  907. package/web/src/components/whatsapp/settings/NotificationsTab.tsx +109 -0
  908. package/web/src/components/whatsapp/settings/OperatorTab.tsx +303 -0
  909. package/web/src/contexts/AuthContext.tsx +103 -0
  910. package/web/src/index.css +130 -0
  911. package/web/src/lib/api.ts +92 -0
  912. package/web/src/lib/utils.ts +6 -0
  913. package/web/src/main.tsx +10 -0
  914. package/web/src/pages/AppointmentDetail.tsx +206 -0
  915. package/web/src/pages/AppointmentValidation.tsx +157 -0
  916. package/web/src/pages/AppointmentValidationDetail.tsx +617 -0
  917. package/web/src/pages/AppointmentValidationNew.tsx +1005 -0
  918. package/web/src/pages/Appointments.tsx +283 -0
  919. package/web/src/pages/Billing.tsx +126 -0
  920. package/web/src/pages/CallDetail.tsx +293 -0
  921. package/web/src/pages/CallSettings.tsx +313 -0
  922. package/web/src/pages/Calls.tsx +188 -0
  923. package/web/src/pages/CampaignDetail.tsx +216 -0
  924. package/web/src/pages/CampaignNew.tsx +277 -0
  925. package/web/src/pages/CampaignResults.tsx +185 -0
  926. package/web/src/pages/Campaigns.tsx +171 -0
  927. package/web/src/pages/Dashboard.tsx +336 -0
  928. package/web/src/pages/InboundSchedule.tsx +246 -0
  929. package/web/src/pages/LeadDetail.tsx +183 -0
  930. package/web/src/pages/Leads.tsx +258 -0
  931. package/web/src/pages/Login.tsx +99 -0
  932. package/web/src/pages/Register.tsx +129 -0
  933. package/web/src/pages/Settings.tsx +133 -0
  934. package/web/src/pages/SurveyDetail.tsx +232 -0
  935. package/web/src/pages/SurveyPreview.tsx +179 -0
  936. package/web/src/pages/Surveys.tsx +207 -0
  937. package/web/src/pages/Users.tsx +199 -0
  938. package/web/src/pages/WhatsApp.tsx +147 -0
  939. package/web/src/pages/WhatsAppSettings.tsx +215 -0
  940. package/web/src/pages/admin/AdminBaileysMessageLog.tsx +331 -0
  941. package/web/src/pages/admin/AdminBaileysRawMessages.tsx +318 -0
  942. package/web/src/pages/admin/AdminConversationReview.tsx +116 -0
  943. package/web/src/pages/admin/AdminCredits.tsx +467 -0
  944. package/web/src/pages/admin/AdminElevenLabsAgentDetail.tsx +332 -0
  945. package/web/src/pages/admin/AdminElevenLabsAgents.tsx +164 -0
  946. package/web/src/pages/admin/AdminElevenLabsBatchCallDetail.tsx +214 -0
  947. package/web/src/pages/admin/AdminElevenLabsBatchCalls.tsx +293 -0
  948. package/web/src/pages/admin/AdminElevenLabsConversationDetail.tsx +230 -0
  949. package/web/src/pages/admin/AdminElevenLabsConversations.tsx +228 -0
  950. package/web/src/pages/admin/AdminElevenLabsInboundCalls.tsx +293 -0
  951. package/web/src/pages/admin/AdminElevenLabsPhoneNumbers.tsx +506 -0
  952. package/web/src/pages/admin/AdminElevenLabsTestChat.tsx +258 -0
  953. package/web/src/pages/admin/AdminElevenLabsWebhooks.tsx +289 -0
  954. package/web/src/pages/admin/AdminEvaluationDashboard.tsx +520 -0
  955. package/web/src/pages/admin/AdminLeads.tsx +339 -0
  956. package/web/src/pages/admin/AdminLogs.tsx +283 -0
  957. package/web/src/pages/admin/AdminMigrations.tsx +247 -0
  958. package/web/src/pages/admin/AdminPlans.tsx +313 -0
  959. package/web/src/pages/admin/AdminSystem.tsx +391 -0
  960. package/web/src/pages/admin/AdminTenantBillableItems.tsx +464 -0
  961. package/web/src/pages/admin/AdminTenantDetail.tsx +1317 -0
  962. package/web/src/pages/admin/AdminTenants.tsx +274 -0
  963. package/web/src/pages/admin/AdminWhatsApp.tsx +618 -0
  964. package/web/src/pages/admin/AdminWhatsAppTestChat.tsx +328 -0
  965. package/web/src/types/index.ts +242 -0
  966. package/web/tsconfig.app.json +32 -0
  967. package/web/tsconfig.json +13 -0
  968. package/web/tsconfig.node.json +26 -0
  969. package/web/vite.config.ts +23 -0
@@ -0,0 +1,2564 @@
1
+ # Database Schema Reference
2
+
3
+ > Portal Asistan 7/24 -- Complete PostgreSQL schema documentation.
4
+ > Every table, column, index, relation, and design decision.
5
+ > Source: Prisma schema from NESTJS_MIGRATION_REFERENCE.md Section 6.2
6
+ > Last updated: 2026-04-02
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Naming Convention](#naming-convention)
13
+ 2. [Enums](#enums)
14
+ 3. [Table Definitions](#table-definitions)
15
+ - [tenants](#1-tenants)
16
+ - [clinics](#2-clinics)
17
+ - [clinic_calendar_connections](#3-clinic_calendar_connections)
18
+ - [clinic_meta_connections](#4-clinic_meta_connections)
19
+ - [phone_numbers](#5-phone_numbers)
20
+ - [users](#6-users)
21
+ - [user_clinic_assignments](#7-user_clinic_assignments)
22
+ - [refresh_tokens](#8-refresh_tokens)
23
+ - [plans](#9-plans)
24
+ - [agents](#10-agents)
25
+ - [calls](#11-calls)
26
+ - [call_details](#12-call_details)
27
+ - [leads](#13-leads)
28
+ - [outbound_campaigns](#14-outbound_campaigns)
29
+ - [outbound_campaign_entries](#15-outbound_campaign_entries)
30
+ - [whatsapp_chats](#16-whatsapp_chats)
31
+ - [whatsapp_sessions](#17-whatsapp_sessions)
32
+ - [whatsapp_messages](#18-whatsapp_messages)
33
+ - ~~whatsapp_contact_profiles~~ — **REMOVED**, replaced by [patients](#36-patients)
34
+ - [operator_requests](#20-operator_requests)
35
+ - [appointments](#21-appointments)
36
+ - [appointment_validation_batches](#22-appointment_validation_batches)
37
+ - [appointment_validation_entries](#23-appointment_validation_entries)
38
+ - [billing_events](#24-billing_events)
39
+ - [payments](#25-payments)
40
+ - [billing_period_snapshots](#26-billing_period_snapshots)
41
+ - [billing_alerts](#27-billing_alerts)
42
+ - [stt_usages](#28-stt_usages)
43
+ - [logs](#29-logs)
44
+ - [audit_logs](#30-audit_logs)
45
+ - [tool_execution_logs](#31-tool_execution_logs)
46
+ - [migrations](#32-migrations)
47
+ - [transcriptions](#33-transcriptions)
48
+ - [summaries](#34-summaries)
49
+ - [whatsapp_recipients](#35-whatsapp_recipients)
50
+ - [patients](#36-patients)
51
+ - [patient_medical_histories](#37-patient_medical_histories)
52
+ - [patient_documents](#38-patient_documents)
53
+ - [patient_notes](#39-patient_notes)
54
+ - [treatments](#40-treatments)
55
+ - [treatment_plans](#41-treatment_plans)
56
+ - [treatment_plan_items](#42-treatment_plan_items)
57
+ - [doctor_profiles](#43-doctor_profiles)
58
+ - [patient_photo_sets](#44-patient_photo_sets)
59
+ - [patient_photos](#45-patient_photos)
60
+ - [examination_records](#46-examination_records)
61
+
62
+ ---
63
+
64
+ ## Naming Convention
65
+
66
+ All column names follow the pattern `{table_prefix}_{field_name}`. This avoids ambiguity in joins and makes it immediately clear which table a column belongs to.
67
+
68
+ | Table | Prefix | Example PK | Example FK |
69
+ |-------|--------|-----------|-----------|
70
+ | tenants | `tenant_` | `tenant_id` | -- |
71
+ | clinics | `clinic_` | `clinic_id` | `clinic_tenant_id` |
72
+ | clinic_calendar_connections | `calcn_` | `calcn_id` | `calcn_clinic_id` |
73
+ | clinic_meta_connections | `meta_` | `meta_id` | `meta_clinic_id` |
74
+ | phone_numbers | `phone_` | `phone_id` | `phone_tenant_id` |
75
+ | users | `user_` | `user_id` | `user_tenant_id` |
76
+ | user_clinic_assignments | `assign_` | `assign_id` | `assign_user_id` |
77
+ | refresh_tokens | `token_` | `token_id` | `token_user_id` |
78
+ | plans | `plan_` | `plan_id` | -- |
79
+ | agents | `agent_` | `agent_id` | `agent_tenant_id` |
80
+ | calls | `call_` | `call_id` | `call_tenant_id` |
81
+ | call_details | `detail_` | `detail_id` | `detail_call_id` |
82
+ | leads | `lead_` | `lead_id` | `lead_tenant_id` |
83
+ | outbound_campaigns | `campaign_` | `campaign_id` | `campaign_tenant_id` |
84
+ | outbound_campaign_entries | `centry_` | `centry_id` | `centry_campaign_id` |
85
+ | whatsapp_chats | `chat_` | `chat_id` | `chat_tenant_id` |
86
+ | whatsapp_sessions | `session_` | `session_id` | `session_tenant_id` |
87
+ | whatsapp_messages | `msg_` | `msg_id` | `msg_tenant_id` |
88
+ | whatsapp_contact_profiles | `contact_` | `contact_id` | `contact_tenant_id` |
89
+ | operator_requests | `opreq_` | `opreq_id` | `opreq_tenant_id` |
90
+ | appointments | `appt_` | `appt_id` | `appt_tenant_id` |
91
+ | appointment_validation_batches | `batch_` | `batch_id` | `batch_tenant_id` |
92
+ | appointment_validation_entries | `bentry_` | `bentry_id` | `bentry_batch_id` |
93
+ | billing_events | `billing_` | `billing_id` | `billing_tenant_id` |
94
+ | payments | `payment_` | `payment_id` | `payment_tenant_id` |
95
+ | billing_period_snapshots | `snapshot_` | `snapshot_id` | `snapshot_tenant_id` |
96
+ | billing_alerts | `alert_` | `alert_id` | `alert_tenant_id` |
97
+ | stt_usages | `stt_` | `stt_id` | `stt_tenant_id` |
98
+ | logs | `log_` | `log_id` | -- |
99
+ | audit_logs | `audit_` | `audit_id` | -- |
100
+ | tool_execution_logs | `toollog_` | `toollog_id` | -- |
101
+ | migrations | `migration_` | `migration_id` | -- |
102
+ | transcriptions | `tx_` | `tx_id` | -- |
103
+ | summaries | `summary_` | `summary_id` | `summary_transcription_id` |
104
+ | whatsapp_recipients | `recipient_` | `recipient_id` | -- |
105
+ | patients | `patient_` | `patient_id` | `patient_tenant_id` |
106
+ | patient_medical_histories | `history_` | `history_id` | `history_patient_id` |
107
+ | patient_documents | `doc_` | `doc_id` | `doc_patient_id` |
108
+ | patient_notes | `note_` | `note_id` | `note_patient_id` |
109
+ | treatments | `treatment_` | `treatment_id` | `treatment_tenant_id` |
110
+ | treatment_plans | `tplan_` | `tplan_id` | `tplan_patient_id` |
111
+ | treatment_plan_items | `tpitem_` | `tpitem_id` | `tpitem_plan_id` |
112
+ | doctor_profiles | `dprofile_` | `dprofile_id` | `dprofile_user_id` |
113
+ | patient_photo_sets | `photoset_` | `photoset_id` | `photoset_patient_id` |
114
+ | patient_photos | `photo_` | `photo_id` | `photo_set_id` |
115
+ | examination_records | `exam_` | `exam_id` | `exam_appointment_id` |
116
+
117
+ **General rules:**
118
+ - Table names: `snake_case`, plural.
119
+ - Primary keys: `{prefix}_id`, UUID, auto-generated with `@default(uuid())`.
120
+ - Foreign keys: `{prefix}_{referenced_table}_id` (e.g., `user_tenant_id` references `tenants.tenant_id`).
121
+ - Timestamps: `{prefix}_created_at`, `{prefix}_updated_at`.
122
+ - All UUIDs are v4 strings.
123
+ - All timestamps are `DateTime` stored as `timestamptz`.
124
+
125
+ ---
126
+
127
+ ## Enums
128
+
129
+ All enums used across the schema. Listed alphabetically.
130
+
131
+ ### AlertService
132
+
133
+ Alert threshold service types for billing alerts.
134
+
135
+ | Value | Description |
136
+ |-------|-------------|
137
+ | `inbound_minutes` | Inbound call minutes usage |
138
+ | `inbound_calls` | Inbound call count |
139
+ | `wa_chats` | WhatsApp billable chat count |
140
+ | `billing_period` | Billing period expiration |
141
+
142
+ ### AlertType
143
+
144
+ Billing alert threshold levels.
145
+
146
+ | Value | Description |
147
+ |-------|-------------|
148
+ | `usage_75` | 75% of plan limit reached |
149
+ | `usage_90` | 90% of plan limit reached |
150
+ | `usage_100` | 100% of plan limit reached (overage begins) |
151
+ | `period_expiring` | Billing period about to expire |
152
+
153
+ ### AppointmentSource
154
+
155
+ How the appointment was created.
156
+
157
+ | Value | Description |
158
+ |-------|-------------|
159
+ | `whatsapp` | Booked via WhatsApp AI chat |
160
+ | `voice_call` | Booked during a voice call |
161
+ | `dashboard` | Manually booked by staff from the admin panel |
162
+ | `walk_in` | Walk-in patient registered at reception |
163
+
164
+ ### AppointmentStatus
165
+
166
+ Current state of an appointment.
167
+
168
+ | Value | Description |
169
+ |-------|-------------|
170
+ | `scheduled` | Booked, upcoming |
171
+ | `confirmed` | Patient confirmed attendance (via validation call or manual) |
172
+ | `in_progress` | Patient is currently being seen |
173
+ | `completed` | Visit finished |
174
+ | `cancelled` | Cancelled before visit |
175
+ | `no_show` | Patient did not show up |
176
+
177
+ ### AppointmentType
178
+
179
+ What kind of visit this appointment represents.
180
+
181
+ | Value | Description |
182
+ |-------|-------------|
183
+ | `appointment` | Standard scheduled appointment |
184
+ | `walk_in` | Walk-in patient (registered on arrival) |
185
+ | `follow_up` | Follow-up visit for a previous treatment |
186
+ | `consultation` | Initial consultation / examination |
187
+
188
+ ### BatchStatus
189
+
190
+ Lifecycle state of an appointment validation batch.
191
+
192
+ | Value | Description |
193
+ |-------|-------------|
194
+ | `draft` | Created, not yet submitted |
195
+ | `submitted` | Sent to ElevenLabs for processing |
196
+ | `in_progress` | Calls are being made |
197
+ | `completed` | All calls finished |
198
+ | `cancelled` | Batch was cancelled before completion |
199
+
200
+ ### BillingEventType
201
+
202
+ Type of billing event for charge tracking.
203
+
204
+ | Value | Description |
205
+ |-------|-------------|
206
+ | `call_charge` | Per-minute charge for a voice call |
207
+ | `subscription` | Recurring subscription charge |
208
+ | `top_up` | Extra minutes purchased |
209
+ | `refund` | Refund issued |
210
+
211
+ ### CallDirection
212
+
213
+ Direction of a voice call.
214
+
215
+ | Value | Description |
216
+ |-------|-------------|
217
+ | `inbound` | Patient called the clinic |
218
+ | `outbound` | System called the patient (campaigns, validation) |
219
+
220
+ ### CallStatus
221
+
222
+ Final outcome of a voice call.
223
+
224
+ | Value | Description |
225
+ |-------|-------------|
226
+ | `completed` | Call connected and ended normally |
227
+ | `failed` | Call could not be placed or errored |
228
+ | `no_answer` | Patient did not pick up |
229
+ | `voicemail` | Call went to voicemail |
230
+ | `transferred` | Call was transferred to another number |
231
+
232
+ ### CampaignEntryStatus
233
+
234
+ Status of a single entry in an outbound campaign.
235
+
236
+ | Value | Description |
237
+ |-------|-------------|
238
+ | `pending` | Waiting to be called |
239
+ | `calling` | Call in progress |
240
+ | `completed` | Call finished successfully |
241
+ | `failed` | Call failed after all retries |
242
+ | `no_answer` | No answer after all retries |
243
+ | `voicemail` | Reached voicemail |
244
+
245
+ ### ChatChannel
246
+
247
+ Which messaging platform the chat originates from.
248
+
249
+ | Value | Description |
250
+ |-------|-------------|
251
+ | `whatsapp` | WhatsApp message |
252
+ | `instagram` | Instagram direct message |
253
+
254
+ ### EntryCallStatus
255
+
256
+ Status of a single entry in an appointment validation batch. Identical values to CampaignEntryStatus but semantically separate.
257
+
258
+ | Value | Description |
259
+ |-------|-------------|
260
+ | `pending` | Waiting to be called |
261
+ | `calling` | Call in progress |
262
+ | `completed` | Call finished |
263
+ | `failed` | Call failed |
264
+ | `no_answer` | No answer |
265
+ | `voicemail` | Reached voicemail |
266
+
267
+ ### LeadStatus
268
+
269
+ Lifecycle stage of a sales lead.
270
+
271
+ | Value | Description |
272
+ |-------|-------------|
273
+ | `new_lead` | Just created (named `new_lead` because `new` is a reserved word) |
274
+ | `contacted` | Staff has reached out |
275
+ | `appointment_scheduled` | Appointment booked |
276
+ | `completed` | Lead converted to patient |
277
+ | `lost` | Lead did not convert |
278
+ | `expired` | Auto-expired after 24 hours with no action |
279
+
280
+ ### MessageSender
281
+
282
+ Who sent a WhatsApp message.
283
+
284
+ | Value | Description |
285
+ |-------|-------------|
286
+ | `contact` | The patient/external person |
287
+ | `ai` | The AI agent |
288
+ | `human` | A human staff member via live chat |
289
+ | `system` | System-generated message (e.g., operator request notification) |
290
+
291
+ ### MigrationStatus
292
+
293
+ Result of a data migration run.
294
+
295
+ | Value | Description |
296
+ |-------|-------------|
297
+ | `success` | Migration completed without errors |
298
+ | `failed` | Migration encountered an error |
299
+
300
+ ### NotificationDeliveryStatus
301
+
302
+ Status of a notification delivery (used for post-call summary notifications).
303
+
304
+ | Value | Description |
305
+ |-------|-------------|
306
+ | `pending` | Not yet sent |
307
+ | `sending` | In the process of being sent |
308
+ | `sent` | Successfully delivered |
309
+ | `failed` | Delivery failed |
310
+
311
+ ### OperatorRequestStatus
312
+
313
+ State of an operator (doctor) request in the operator-in-the-loop workflow.
314
+
315
+ | Value | Description |
316
+ |-------|-------------|
317
+ | `pending` | Forwarded to doctor, awaiting response |
318
+ | `responded` | Doctor replied with price/instructions |
319
+ | `expired` | Doctor did not respond within timeout |
320
+ | `cancelled` | Request was cancelled |
321
+
322
+ ### OutboundCampaignStatus
323
+
324
+ Lifecycle state of an outbound campaign.
325
+
326
+ | Value | Description |
327
+ |-------|-------------|
328
+ | `draft` | Being configured, not yet started |
329
+ | `scheduled` | Configured and waiting for scheduled start time |
330
+ | `running` | Actively making calls |
331
+ | `paused` | Temporarily paused (can be resumed) |
332
+ | `completed` | All entries processed |
333
+
334
+ ### PaymentMethod
335
+
336
+ How a tenant payment was made.
337
+
338
+ | Value | Description |
339
+ |-------|-------------|
340
+ | `cash` | Cash payment |
341
+ | `bank_transfer` | Wire/bank transfer |
342
+ | `other` | Other payment method |
343
+
344
+ ### PaymentType
345
+
346
+ Type of tenant payment.
347
+
348
+ | Value | Description |
349
+ |-------|-------------|
350
+ | `subscription` | Monthly/periodic subscription |
351
+ | `top_up` | Extra minutes or usage purchase |
352
+
353
+ ### PhoneNumberProvider
354
+
355
+ Which system manages the phone number.
356
+
357
+ | Value | Description |
358
+ |-------|-------------|
359
+ | `meta_business` | Official Meta Business Cloud API |
360
+ | `elevenlabs` | ElevenLabs SIP trunk (voice calls) |
361
+
362
+ ### PhoneNumberPurpose
363
+
364
+ What the phone number is used for.
365
+
366
+ | Value | Description |
367
+ |-------|-------------|
368
+ | `whatsapp` | WhatsApp messaging |
369
+ | `inbound_call` | Receiving voice calls |
370
+ | `outbound_call` | Making voice calls (campaigns, validation) |
371
+
372
+ ### PostCallStrategy
373
+
374
+ What happens after a voice call ends.
375
+
376
+ | Value | Description |
377
+ |-------|-------------|
378
+ | `summarize` | AI generates a summary of the call |
379
+ | `none` | No post-call processing |
380
+
381
+ ### Sentiment
382
+
383
+ AI-detected sentiment of a conversation.
384
+
385
+ | Value | Description |
386
+ |-------|-------------|
387
+ | `positive` | Positive tone/outcome |
388
+ | `neutral` | Neutral tone |
389
+ | `negative` | Negative tone/outcome |
390
+
391
+ ### SessionResolvedBy
392
+
393
+ How a WhatsApp session was resolved (ended).
394
+
395
+ | Value | Description |
396
+ |-------|-------------|
397
+ | `human` | Staff took over and resolved |
398
+ | `ai_timeout` | AI finished conversation naturally |
399
+ | `timeout` | Session expired due to inactivity (2-hour sweep) |
400
+ | `manual` | Staff manually resolved without taking over |
401
+
402
+ ### SessionStatus
403
+
404
+ Current state of a WhatsApp chat session.
405
+
406
+ | Value | Description |
407
+ |-------|-------------|
408
+ | `waiting` | Grace period active, waiting to see if human responds first |
409
+ | `active` | AI is handling the conversation |
410
+ | `resolved` | Session is closed |
411
+
412
+ ### TranscriptionProcessingStatus
413
+
414
+ Pipeline state for post-call transcription processing.
415
+
416
+ | Value | Description |
417
+ |-------|-------------|
418
+ | `pending` | Transcription received, not yet summarized |
419
+ | `summarizing` | AI summary generation in progress |
420
+ | `summarized` | Summary complete |
421
+ | `notified` | Summary notification sent to staff |
422
+ | `failed` | Processing failed |
423
+
424
+ ### UserRole
425
+
426
+ User roles in the system. Platform roles (superadmin, sales) have no tenant. Tenant roles are scoped to a tenant and further restricted by clinic assignments.
427
+
428
+ | Value | Description |
429
+ |-------|-------------|
430
+ | `superadmin` | Platform operator -- all endpoints, all tenants, bypasses all checks |
431
+ | `sales` | Platform sales team -- create tenants, assign plans, no access to tenant data |
432
+ | `owner` | Clinic chain owner -- full access to all clinics within the tenant |
433
+ | `admin` | Branch administrator -- full access to assigned clinics |
434
+ | `doctor` | Doctor/specialist -- patient history, operator requests, appointments |
435
+ | `receptionist` | Front desk staff -- live chat, appointments, calls, leads |
436
+
437
+ ### ValidationResult
438
+
439
+ Outcome of an appointment validation call.
440
+
441
+ | Value | Description |
442
+ |-------|-------------|
443
+ | `confirmed` | Patient confirmed the appointment |
444
+ | `cancelled` | Patient wants to cancel |
445
+ | `reschedule` | Patient wants to reschedule |
446
+ | `unresolved` | Could not determine outcome (e.g., call too short, no tool fired) |
447
+
448
+ ### TreatmentPlanStatus
449
+
450
+ Lifecycle state of a treatment plan.
451
+
452
+ | Value | Description |
453
+ |-------|-------------|
454
+ | `active` | Plan is in progress, items being worked on |
455
+ | `completed` | All items completed |
456
+ | `cancelled` | Plan was cancelled |
457
+
458
+ ### TreatmentPlanItemStatus
459
+
460
+ Status of a single item within a treatment plan.
461
+
462
+ | Value | Description |
463
+ |-------|-------------|
464
+ | `planned` | Not yet scheduled |
465
+ | `scheduled` | Appointment linked |
466
+ | `in_progress` | Currently being performed |
467
+ | `completed` | Done |
468
+ | `cancelled` | Cancelled |
469
+
470
+ ---
471
+
472
+ ## Table Definitions
473
+
474
+ ---
475
+
476
+ ### 1. tenants
477
+
478
+ **Purpose:** Root entity for multi-tenancy. Each tenant is a healthcare clinic or clinic chain. The tenant is a **thin shell** — it holds identity, billing, feature flags, and compliance. All operational configuration lives on the clinic level.
479
+
480
+ | Column | Type | Required | Default | Description |
481
+ |--------|------|----------|---------|-------------|
482
+ | `tenant_id` | UUID | Yes | `uuid()` | Primary key |
483
+ | `tenant_name` | String | Yes | -- | Display name (e.g., "Gueluestuek Dental") |
484
+ | `tenant_plan_id` | UUID | No | `null` | FK to `plans`. Null means no active plan |
485
+ | `tenant_enabled_features` | String[] | Yes | `[]` | Feature flags. See Feature Flags section in system overview |
486
+ | `tenant_is_active` | Boolean | Yes | `true` | Soft-disable toggle. Inactive tenants cannot use the system |
487
+ | `tenant_deleted_at` | DateTime | No | `null` | Soft-delete timestamp. Non-null means tenant is deleted |
488
+ | `tenant_kvkk_consent` | Boolean | Yes | `false` | Whether tenant has accepted KVKK/GDPR data processing terms |
489
+ | `tenant_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
490
+ | `tenant_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
491
+
492
+ **Indexes:**
493
+
494
+ | Index | Columns | Purpose |
495
+ |-------|---------|---------|
496
+ | Primary | `tenant_id` | PK lookup |
497
+ | `idx_tenants_plan` | `tenant_plan_id` | Find tenants by plan (billing, plan changes) |
498
+
499
+ **Relations:**
500
+
501
+ | Direction | Target | FK Column | Description |
502
+ |-----------|--------|-----------|-------------|
503
+ | belongs_to | plans | `tenant_plan_id` | Current subscription plan |
504
+ | has_many | users, clinics, agents, phone_numbers, patients, treatments, calls, leads, outbound_campaigns, whatsapp_chats, whatsapp_sessions, whatsapp_messages, operator_requests, appointments, appointment_validation_batches, billing_events, payments, billing_period_snapshots, billing_alerts, stt_usages | -- | All business data |
505
+
506
+ **Design decisions:**
507
+ - **Tenant is thin.** All operational configuration (language, AI provider, grace period, notification phones, operator settings) lives on the clinic. No inheritance chain — each clinic is self-contained with its own defaults.
508
+ - Feature flags are stored as a simple `String[]` rather than a separate table. The list is small (13 flags), queried on every request (embedded in JWT), and rarely updated.
509
+ - Billing (`plan_id`) is tenant-level because a clinic chain pays one subscription, not per-branch.
510
+ - KVKK consent is tenant-level because it is a legal agreement by the business entity.
511
+ - Soft delete via `tenant_deleted_at` rather than hard delete to preserve billing history and audit trail.
512
+
513
+ **What moved to clinic:** Language, AI provider/model, SMS config, notification phones, grace period, operator prefix/timeout, appointment duration, profile agent. See [clinics](#2-clinics).
514
+
515
+ ---
516
+
517
+ ### 2. clinics
518
+
519
+ **Purpose:** Represents a physical clinic branch/location within a tenant. A single-clinic tenant has exactly one clinic (auto-created, `clinic_is_default = true`). Multi-clinic tenants have one per branch. The clinic is the **primary configuration entity** — all operational settings live here. No inheritance from tenant.
520
+
521
+ | Column | Type | Required | Default | Description |
522
+ |--------|------|----------|---------|-------------|
523
+ | `clinic_id` | UUID | Yes | `uuid()` | Primary key |
524
+ | `clinic_tenant_id` | UUID | Yes | -- | FK to `tenants` |
525
+ | `clinic_name` | String | Yes | -- | Display name (e.g., "Kadikoy Branch") |
526
+ | `clinic_slug` | String | Yes | -- | URL-friendly identifier (e.g., `"kadikoy"`, `"main"`) |
527
+ | `clinic_address` | String | Yes | `""` | Physical address |
528
+ | `clinic_phone` | String | Yes | `""` | Clinic contact phone (for display, not WhatsApp routing) |
529
+ | `clinic_email` | String | Yes | `""` | Clinic contact email |
530
+ | `clinic_timezone` | String | Yes | `"Europe/Istanbul"` | IANA timezone |
531
+ | `clinic_is_default` | Boolean | Yes | `false` | True for the auto-created first clinic in single-clinic tenants |
532
+ | `clinic_is_active` | Boolean | Yes | `true` | Soft-disable toggle |
533
+ | `clinic_language` | String | Yes | `"tr"` | Default language for AI agents and UI at this clinic |
534
+ | `clinic_ai_provider` | String | Yes | `"openai"` | AI provider: `"openai"` or `"anthropic"` |
535
+ | `clinic_ai_model` | String | Yes | `""` | AI model override. Empty = use provider default |
536
+ | `clinic_grace_period_seconds` | Int | Yes | `180` | Seconds before AI activates on a new message |
537
+ | `clinic_operator_phone` | String | No | `null` | Doctor's WhatsApp number for operator workflow |
538
+ | `clinic_operator_prefix` | String | Yes | `"R"` | REF code prefix for operator requests (e.g., `R-AB2X`) |
539
+ | `clinic_operator_timeout_hours` | Int | Yes | `24` | Hours before an operator request expires |
540
+ | `clinic_ai_shift_enabled` | Boolean | Yes | `false` | Whether AI only operates during specific hours |
541
+ | `clinic_ai_shift_schedule` | Json | Yes | `"[]"` | Array of `{day, start, end}` objects defining AI operating hours |
542
+ | `clinic_blacklisted_numbers` | String[] | Yes | `[]` | Phone numbers blocked from this clinic's AI agent |
543
+ | `clinic_fixed_first_message` | String | No | `null` | Fixed first message sent to new contacts. Null = disabled |
544
+ | `clinic_sms_enabled` | Boolean | Yes | `false` | Whether SMS notifications are enabled |
545
+ | `clinic_sms_phones` | String[] | Yes | `[]` | Staff phone numbers for SMS notifications |
546
+ | `clinic_notification_phones` | String[] | Yes | `[]` | General notification recipient phone numbers |
547
+ | `clinic_message_retention_days` | Int | Yes | `7` | Days to keep WhatsApp/Instagram messages before auto-deletion |
548
+ | `clinic_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
549
+ | `clinic_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
550
+
551
+ **Indexes:**
552
+
553
+ | Index | Columns | Purpose |
554
+ |-------|---------|---------|
555
+ | Primary | `clinic_id` | PK lookup |
556
+ | Unique | `(clinic_tenant_id, clinic_slug)` | Ensure slug uniqueness within a tenant |
557
+ | `idx_clinics_tenant` | `clinic_tenant_id` | List all clinics for a tenant |
558
+ | `idx_clinics_operator` | `(clinic_tenant_id, clinic_operator_phone)` | Look up clinic when doctor replies to operator request |
559
+
560
+ **Relations:**
561
+
562
+ | Direction | Target | FK Column | Description |
563
+ |-----------|--------|-----------|-------------|
564
+ | belongs_to | tenants | `clinic_tenant_id` | Parent tenant (cascade delete) |
565
+ | has_many | user_clinic_assignments | -- | Users assigned to this clinic |
566
+ | has_many | agents, phone_numbers | -- | Clinic-scoped agents and numbers |
567
+ | has_one | clinic_calendar_connections | -- | Google Calendar connection |
568
+ | has_many | clinic_meta_connections | -- | Meta Business API connections (WhatsApp + Instagram) |
569
+ | has_many | patients, treatments, treatment_plans | -- | Clinic-scoped CRM data |
570
+ | has_many | whatsapp_chats, whatsapp_sessions, appointments, operator_requests, calls, leads, outbound_campaigns, appointment_validation_batches | -- | Clinic-scoped operational data |
571
+
572
+ **Design decisions:**
573
+ - **Clinic owns all config.** No inheritance from tenant. Each clinic is self-contained. When a new clinic is created, it gets sensible defaults. This eliminates the "check clinic → fall back to tenant → fall back to system" resolution chain.
574
+ - All config fields are **non-nullable with defaults**. No null-means-inherit pattern.
575
+ - `clinic_phone` and `clinic_email` are for display purposes (e.g., shown on appointment confirmations). They are NOT the WhatsApp number — WhatsApp routing uses the `phone_numbers` table.
576
+ - `clinic_settings` JSONB was removed. All config is now in proper columns. If edge-case config is needed later, it can be added back.
577
+ - Operator workflow fields are top-level columns because they are queried on every incoming WhatsApp message.
578
+ - `clinic_ai_provider` / `clinic_ai_model` control which AI provider is used for background processing (summaries, labels, extraction) at this clinic. Per-agent overrides still exist on the `agents` table.
579
+
580
+ ---
581
+
582
+ ### 3. clinic_calendar_connections
583
+
584
+ **Purpose:** Stores Google Calendar OAuth2 connection details for a clinic. Extracted from JSONB to enable proper indexing and encrypted token storage. One-to-one with `clinics`.
585
+
586
+ | Column | Type | Required | Default | Description |
587
+ |--------|------|----------|---------|-------------|
588
+ | `calcn_id` | UUID | Yes | `uuid()` | Primary key |
589
+ | `calcn_clinic_id` | UUID | Yes (unique) | -- | FK to `clinics`. One-to-one relationship |
590
+ | `calcn_google_email` | String | No | `null` | Google account email used for calendar |
591
+ | `calcn_encrypted_refresh_token` | String | No | `null` | AES-256-GCM encrypted OAuth2 refresh token |
592
+ | `calcn_calendar_id` | String | No | `null` | Google Calendar ID (e.g., `primary` or specific calendar) |
593
+ | `calcn_calendar_name` | String | No | `null` | Display name of the selected calendar |
594
+ | `calcn_appointment_duration` | Int | Yes | `30` | Default appointment duration in minutes. Overridden by treatment or doctor duration when specified |
595
+ | `calcn_working_hours` | Json | Yes | `"[]"` | Array of `{day, start, end}` objects defining clinic-wide appointment hours |
596
+ | `calcn_blocked_dates` | Json | Yes | `"[]"` | Array of `{date, reason}` objects for holidays/closures |
597
+ | `calcn_connected_at` | DateTime | No | `null` | When the Google Calendar was connected |
598
+ | `calcn_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
599
+ | `calcn_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
600
+
601
+ **Indexes:**
602
+
603
+ | Index | Columns | Purpose |
604
+ |-------|---------|---------|
605
+ | Primary | `calcn_id` | PK lookup |
606
+ | Unique | `calcn_clinic_id` | Enforce one-to-one with clinics |
607
+
608
+ **Relations:**
609
+
610
+ | Direction | Target | FK Column | Description |
611
+ |-----------|--------|-----------|-------------|
612
+ | belongs_to | clinics | `calcn_clinic_id` | Parent clinic (cascade delete) |
613
+
614
+ **Design decisions:**
615
+ - One-to-one with clinics via unique constraint on `calcn_clinic_id`.
616
+ - Timezone is not stored here — uses `clinic.clinic_timezone` for all Calendar API calls.
617
+ - Refresh token is AES-256-GCM encrypted using `GOOGLE_TOKEN_ENCRYPTION_KEY`. Access tokens are cached in Redis (`gcal:token:{clinicId}`) with a 50-minute TTL.
618
+ - `calcn_appointment_duration` is the clinic-wide default. Priority chain for slot duration: `treatment.duration_minutes` > `doctor.profile_appointment_duration` > `calcn_appointment_duration` (30 min).
619
+ - Working hours define clinic-wide availability. Doctor-specific hours are on `doctor_profiles.profile_working_hours`. Both are checked during slot generation.
620
+ - Working hours and blocked dates remain as JSONB because they are variable-length arrays loaded in bulk (never queried individually).
621
+
622
+ ---
623
+
624
+ ### 4. clinic_meta_connections
625
+
626
+ **Purpose:** Stores Meta Business API connection details for a clinic. Each row is one connection — either a WhatsApp Business Account (WABA) or an Instagram page. A clinic can have multiple of each.
627
+
628
+ | Column | Type | Required | Default | Description |
629
+ |--------|------|----------|---------|-------------|
630
+ | `meta_id` | UUID | Yes | `uuid()` | Primary key |
631
+ | `meta_clinic_id` | UUID | Yes | -- | FK to `clinics`. Many-to-one (a clinic can have multiple connections) |
632
+ | `meta_type` | String | Yes | -- | Connection type: `whatsapp` or `instagram` |
633
+ | `meta_label` | String | Yes | `""` | Display label (e.g., "Main WhatsApp", "Dr. Ayse Instagram") |
634
+ | `meta_waba_id` | String | No | `null` | WhatsApp Business Account ID. Set when `meta_type = 'whatsapp'` |
635
+ | `meta_phone_number_id` | String | No | `null` | Meta-assigned phone number ID. Set when `meta_type = 'whatsapp'` |
636
+ | `meta_display_phone` | String | No | `null` | Human-readable phone number. Set when `meta_type = 'whatsapp'` |
637
+ | `meta_instagram_page_id` | String | No | `null` | Instagram Business Account page ID. Set when `meta_type = 'instagram'` |
638
+ | `meta_encrypted_access_token` | String | No | `null` | AES-256-GCM encrypted access token (WABA token or page token depending on type) |
639
+ | `meta_is_active` | Boolean | Yes | `true` | Whether this connection is active |
640
+ | `meta_connected_at` | DateTime | No | `null` | When this connection was established |
641
+ | `meta_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
642
+ | `meta_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
643
+
644
+ **Indexes:**
645
+
646
+ | Index | Columns | Purpose |
647
+ |-------|---------|---------|
648
+ | Primary | `meta_id` | PK lookup |
649
+ | `idx_meta_clinic` | `meta_clinic_id` | List all connections for a clinic |
650
+ | `idx_meta_clinic_type` | `(meta_clinic_id, meta_type)` | List WhatsApp or Instagram connections for a clinic |
651
+ | `idx_meta_phone_number` | `meta_phone_number_id` | Webhook routing: resolve incoming WhatsApp message to clinic |
652
+ | `idx_meta_instagram_page` | `meta_instagram_page_id` | Webhook routing: resolve incoming Instagram DM to clinic |
653
+
654
+ **Relations:**
655
+
656
+ | Direction | Target | FK Column | Description |
657
+ |-----------|--------|-----------|-------------|
658
+ | belongs_to | clinics | `meta_clinic_id` | Parent clinic (cascade delete) |
659
+
660
+ **Design decisions:**
661
+ - **1:many with clinic** (not 1:1). A clinic can have multiple WABAs (rare but possible — different brands) and multiple Instagram pages (common — main brand + individual doctor pages).
662
+ - Meta Business API is the **sole WhatsApp provider** in the system. No Baileys/self-hosted bridge.
663
+ - `meta_type` determines which fields are populated. WhatsApp connections use `waba_id`, `phone_number_id`, `display_phone`. Instagram connections use `instagram_page_id`. Both use `encrypted_access_token`.
664
+ - Single `encrypted_access_token` field works for both types. For WhatsApp it's the WABA access token. For Instagram it's the page access token. Both encrypted with `META_TOKEN_ENCRYPTION_KEY`.
665
+ - Old design had separate `encrypted_page_token` for Instagram — merged into `encrypted_access_token` since each row is now one connection type.
666
+ - `meta_phone_number_id` and `meta_instagram_page_id` are indexed for webhook routing. When a Meta webhook arrives, the system looks up the connection by phone_number_id or page_id to resolve the clinic.
667
+
668
+ ---
669
+
670
+ ### 5. phone_numbers
671
+
672
+ **Purpose:** Central registry for all phone numbers across all uses. Each number has one purpose (WhatsApp, inbound call, or outbound call) and links to a clinic and agent for routing. This is how the system knows which clinic and AI agent handle an incoming message or call.
673
+
674
+ | Column | Type | Required | Default | Description |
675
+ |--------|------|----------|---------|-------------|
676
+ | `phone_id` | UUID | Yes | `uuid()` | Primary key |
677
+ | `phone_tenant_id` | UUID | Yes | -- | FK to `tenants` |
678
+ | `phone_clinic_id` | UUID | No | `null` | FK to `clinics`. Null = shared across clinics (e.g., outbound line for the whole chain) |
679
+ | `phone_number` | String | Yes | -- | Phone number in E.164 format (e.g., `+905551234567`) |
680
+ | `phone_label` | String | Yes | `""` | Human-readable label (e.g., "Main WhatsApp", "Reception Line") |
681
+ | `phone_purpose` | PhoneNumberPurpose | Yes | -- | `whatsapp`, `inbound_call`, or `outbound_call` |
682
+ | `phone_provider` | PhoneNumberProvider | Yes | -- | `meta_business` or `elevenlabs` |
683
+ | `phone_external_account_id` | String | No | `null` | Meta phone_number_id (for WhatsApp numbers) |
684
+ | `phone_elevenlabs_phone_id` | String | No | `null` | ElevenLabs phone number ID (for voice call numbers) |
685
+ | `phone_elevenlabs_agent_id` | String | No | `null` | ElevenLabs agent ID (external, on the ElevenLabs side) |
686
+ | `phone_agent_id` | UUID | No | `null` | FK to `agents`. Internal agent routing |
687
+ | `phone_wa_connected` | Boolean | Yes | `false` | Whether WhatsApp is currently connected on this number |
688
+ | `phone_wa_connected_at` | DateTime | No | `null` | When WhatsApp was last connected |
689
+ | `phone_wa_bridge_provider` | String | No | `null` | WhatsApp bridge: `meta_business` or `baileys` (Baileys reserved for future) |
690
+ | `phone_schedule_enabled` | Boolean | Yes | `false` | Whether time-based scheduling is active (inbound call numbers) |
691
+ | `phone_schedule` | Json | Yes | `"[]"` | Array of `{day, start, end}` schedule windows for agent assignment |
692
+ | `phone_agent_assigned` | Boolean | Yes | `false` | Whether the ElevenLabs agent is currently assigned to this number |
693
+ | `phone_is_active` | Boolean | Yes | `true` | Soft-disable toggle |
694
+ | `phone_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
695
+ | `phone_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
696
+
697
+ **Indexes:**
698
+
699
+ | Index | Columns | Purpose |
700
+ |-------|---------|---------|
701
+ | Primary | `phone_id` | PK lookup |
702
+ | Unique | `(phone_tenant_id, phone_number, phone_purpose)` | Prevent duplicate registration of the same number for the same purpose |
703
+ | `idx_phones_tenant_purpose` | `(phone_tenant_id, phone_purpose)` | List numbers by purpose |
704
+ | `idx_phones_clinic` | `phone_clinic_id` | List numbers assigned to a clinic |
705
+ | `idx_phones_provider_external` | `(phone_provider, phone_external_account_id)` | Resolve Meta webhook to our phone number record |
706
+
707
+ **Relations:**
708
+
709
+ | Direction | Target | FK Column | Description |
710
+ |-----------|--------|-----------|-------------|
711
+ | belongs_to | tenants | `phone_tenant_id` | Parent tenant (cascade delete) |
712
+ | belongs_to | clinics | `phone_clinic_id` | Assigned clinic (optional) |
713
+ | belongs_to | agents | `phone_agent_id` | Internal agent routing |
714
+ | has_many | whatsapp_chats | -- | Chats received on this number |
715
+ | has_many | outbound_campaigns | -- | Campaigns using this number for outbound calls |
716
+ | has_many | appointment_validation_batches | -- | Validation batches using this number |
717
+
718
+ **Design decisions:**
719
+ - The unique constraint `(tenant_id, number, purpose)` allows the same physical number to serve multiple purposes (e.g., a number can be both `whatsapp` and `inbound_call`).
720
+ - `phone_elevenlabs_agent_id` is the external ElevenLabs agent ID, while `phone_agent_id` is the FK to our internal `agents` table. Both are needed because the internal agent stores our config while the ElevenLabs ID is used for API calls.
721
+ - `phone_wa_connected`, `phone_wa_connected_at`, `phone_wa_bridge_provider` are kept for future Baileys integration. At launch, only `meta_business` is used as bridge provider. Fields default to false/null.
722
+ - Schedule fields (`phone_schedule_enabled`, `phone_schedule`) are for inbound call numbers only — controls when the ElevenLabs agent is assigned/unassigned via the 15-minute schedule sync job.
723
+ - `phone_clinic_id` is nullable because outbound call numbers may be shared across clinics in a multi-branch chain.
724
+
725
+ ---
726
+
727
+ ### 6. users
728
+
729
+ **Purpose:** User accounts for the platform. Platform-level users (superadmin, sales) have `user_tenant_id = null`. Tenant-level users (owner, admin, doctor, receptionist) are scoped to a tenant and further restricted by clinic assignments.
730
+
731
+ | Column | Type | Required | Default | Description |
732
+ |--------|------|----------|---------|-------------|
733
+ | `user_id` | UUID | Yes | `uuid()` | Primary key |
734
+ | `user_tenant_id` | UUID | No | `null` | FK to `tenants`. Null for platform roles (superadmin, sales) |
735
+ | `user_email` | String | Yes (unique) | -- | Login email. Globally unique across all tenants |
736
+ | `user_password_hash` | String | Yes | -- | bcrypt hash (12 rounds) |
737
+ | `user_role` | UserRole | Yes | `receptionist` | One of: `superadmin`, `sales`, `owner`, `admin`, `doctor`, `receptionist` |
738
+ | `user_first_name` | String | Yes | -- | First name |
739
+ | `user_last_name` | String | Yes | -- | Last name |
740
+ | `user_phone` | String | No | `null` | Staff phone number (E.164). Used for SMS notifications, contact info |
741
+ | `user_is_active` | Boolean | Yes | `true` | Soft-disable toggle. Inactive users cannot log in |
742
+ | `user_last_login_at` | DateTime | No | `null` | Last successful login timestamp |
743
+ | `user_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
744
+ | `user_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
745
+
746
+ **Indexes:**
747
+
748
+ | Index | Columns | Purpose |
749
+ |-------|---------|---------|
750
+ | Primary | `user_id` | PK lookup |
751
+ | Unique | `user_email` | Login lookup, prevent duplicate accounts |
752
+ | `idx_users_tenant` | `user_tenant_id` | List all users for a tenant |
753
+
754
+ **Relations:**
755
+
756
+ | Direction | Target | FK Column | Description |
757
+ |-----------|--------|-----------|-------------|
758
+ | belongs_to | tenants | `user_tenant_id` | Parent tenant (cascade delete). Null for platform roles |
759
+ | has_one | doctor_profiles | -- | Doctor-specific profile (specialty, working hours). Only for doctor role |
760
+ | has_many | user_clinic_assignments | -- | Which clinics this user can access |
761
+ | has_many | refresh_tokens | -- | Active refresh tokens |
762
+ | has_many | whatsapp_messages | -- | Messages sent as human agent (`sender = 'human'`) |
763
+ | has_many | whatsapp_sessions (TakenOverBy) | -- | Sessions this user took over |
764
+ | has_many | payments (RecordedBy) | -- | Payments recorded by this user |
765
+ | has_many | appointment_validation_batches (CreatedBy) | -- | Validation batches created by this user |
766
+ | has_many | treatment_plans (PlanDoctor) | -- | Treatment plans managed by this doctor |
767
+ | has_many | patient_notes (Author) | -- | Patient notes written by this user |
768
+ | has_many | patient_documents (UploadedBy) | -- | Documents uploaded by this user |
769
+ | has_many | patient_photo_sets (TakenBy) | -- | Photo sets taken by this user |
770
+
771
+ **Design decisions:**
772
+ - Email is globally unique (not per-tenant). A person can only have one account.
773
+ - `user_tenant_id` is nullable so platform roles (superadmin, sales) can exist without a tenant.
774
+ - `user_first_name` + `user_last_name` for consistency with the `patients` table. Display name computed as `"{first_name} {last_name}"`.
775
+ - `user_phone` is optional. Used for SMS notifications to staff and as contact info in audit trail.
776
+ - Password stored as bcrypt hash with 12 rounds. No plaintext, no reversible encryption.
777
+ - 2FA (TOTP) can be added later as additional columns (`twoFactorSecret`, `twoFactorEnabled`, `twoFactorBackupCodes`). Not included at launch.
778
+
779
+ ---
780
+
781
+ ### 7. user_clinic_assignments
782
+
783
+ **Purpose:** Junction table linking users to the clinics they can access. `owner` role users bypass this (they see all clinics). `admin`, `doctor`, and `receptionist` users only see data from their assigned clinics.
784
+
785
+ | Column | Type | Required | Default | Description |
786
+ |--------|------|----------|---------|-------------|
787
+ | `assign_id` | UUID | Yes | `uuid()` | Primary key |
788
+ | `assign_user_id` | UUID | Yes | -- | FK to `users` |
789
+ | `assign_clinic_id` | UUID | Yes | -- | FK to `clinics` |
790
+ | `assign_created_at` | DateTime | Yes | `now()` | When the assignment was created |
791
+
792
+ **Indexes:**
793
+
794
+ | Index | Columns | Purpose |
795
+ |-------|---------|---------|
796
+ | Primary | `assign_id` | PK lookup |
797
+ | Unique | `(assign_user_id, assign_clinic_id)` | Prevent duplicate assignments |
798
+ | `idx_assign_user` | `assign_user_id` | Get all clinics for a user (used by ClinicAccessGuard on every request) |
799
+ | `idx_assign_clinic` | `assign_clinic_id` | Get all users for a clinic (user management page) |
800
+
801
+ **Relations:**
802
+
803
+ | Direction | Target | FK Column | Description |
804
+ |-----------|--------|-----------|-------------|
805
+ | belongs_to | users | `assign_user_id` | The user (cascade delete) |
806
+ | belongs_to | clinics | `assign_clinic_id` | The clinic (cascade delete) |
807
+
808
+ **Design decisions:**
809
+ - Simple junction table rather than embedding clinic IDs in the user record. This allows proper foreign key enforcement and easy querying from both directions.
810
+ - Cascade delete on both sides: if a user is deleted, their assignments are removed. If a clinic is deleted, all assignments to it are removed.
811
+
812
+ ---
813
+
814
+ ### 8. refresh_tokens
815
+
816
+ **Purpose:** Stores active refresh tokens for JWT authentication. Each login creates a new refresh token. Tokens are rotated on refresh (old token deleted, new one created).
817
+
818
+ | Column | Type | Required | Default | Description |
819
+ |--------|------|----------|---------|-------------|
820
+ | `token_id` | UUID | Yes | `uuid()` | Primary key |
821
+ | `token_user_id` | UUID | Yes | -- | FK to `users` |
822
+ | `token_value` | String | Yes (unique) | -- | The refresh token string (opaque) |
823
+ | `token_expires_at` | DateTime | Yes | -- | When this token expires (30 days from creation) |
824
+ | `token_created_at` | DateTime | Yes | `now()` | When this token was created |
825
+
826
+ **Indexes:**
827
+
828
+ | Index | Columns | Purpose |
829
+ |-------|---------|---------|
830
+ | Primary | `token_id` | PK lookup |
831
+ | Unique | `token_value` | Token validation lookup |
832
+ | `idx_tokens_user` | `token_user_id` | Revoke all tokens for a user (logout from all devices) |
833
+ | `idx_tokens_expires` | `token_expires_at` | Cleanup expired tokens (BullMQ job) |
834
+
835
+ **Relations:**
836
+
837
+ | Direction | Target | FK Column | Description |
838
+ |-----------|--------|-----------|-------------|
839
+ | belongs_to | users | `token_user_id` | Token owner (cascade delete) |
840
+
841
+ **Design decisions:**
842
+ - Refresh tokens live in PostgreSQL for persistence, plus a Redis set for fast revocation checks.
843
+ - Token rotation: on each refresh, the old token is deleted and a new one is created. This limits the window of a stolen token.
844
+ - Access tokens (15-minute TTL) are stateless JWTs -- not stored in the database. Revoked access tokens are tracked in a Redis blacklist that expires with the token's TTL.
845
+
846
+ ---
847
+
848
+ ### 9. plans
849
+
850
+ **Purpose:** Subscription plan definitions. Each plan specifies usage limits (call minutes, chat count) and pricing. Tenants are assigned one plan at a time.
851
+
852
+ | Column | Type | Required | Default | Description |
853
+ |--------|------|----------|---------|-------------|
854
+ | `plan_id` | UUID | Yes | `uuid()` | Primary key |
855
+ | `plan_name` | String | Yes | -- | Display name (e.g., "Starter", "Professional") |
856
+ | `plan_slug` | String | Yes (unique) | -- | URL-friendly identifier (e.g., `"starter"`, `"pro"`) |
857
+ | `plan_price` | Decimal | Yes | -- | Monthly price in TRY |
858
+ | `plan_overage_rate` | Decimal | Yes | -- | Per-minute charge when usage exceeds plan limits (TRY) |
859
+ | `plan_included_inbound_minutes` | Int | Yes | `0` | Included inbound call minutes per billing period |
860
+ | `plan_included_inbound_calls` | Int | Yes | `0` | Included inbound call count per billing period |
861
+ | `plan_included_outbound_minutes` | Int | Yes | `0` | Included outbound call minutes per billing period |
862
+ | `plan_included_outbound_calls` | Int | Yes | `0` | Included outbound call count per billing period |
863
+ | `plan_included_wa_chats` | Int | Yes | `0` | Included WhatsApp billable chats per billing period |
864
+ | `plan_is_active` | Boolean | Yes | `true` | Whether this plan can be assigned to new tenants |
865
+ | `plan_sort_order` | Int | Yes | `0` | Display ordering in plan selection UI |
866
+ | `plan_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
867
+ | `plan_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
868
+
869
+ **Indexes:**
870
+
871
+ | Index | Columns | Purpose |
872
+ |-------|---------|---------|
873
+ | Primary | `plan_id` | PK lookup |
874
+ | Unique | `plan_slug` | Lookup by slug (API, UI) |
875
+
876
+ **Relations:**
877
+
878
+ | Direction | Target | FK Column | Description |
879
+ |-----------|--------|-----------|-------------|
880
+ | has_many | tenants | -- | Tenants on this plan |
881
+ | has_many | billing_period_snapshots | -- | Historical snapshots referencing this plan |
882
+
883
+ **Design decisions:**
884
+ - Plans are global (not per-tenant). Created and managed by superadmin.
885
+ - `Decimal` type for `plan_price` and `plan_overage_rate` to avoid floating-point rounding issues with currency.
886
+ - `plan_sort_order` allows the admin to control the order plans appear in the UI without relying on alphabetical or creation-date sorting.
887
+
888
+ ---
889
+
890
+ ### 10. agents
891
+
892
+ > **TODO:** Revisit this model later. Column structure depends on final ElevenLabs agent configuration decisions.
893
+
894
+ **Purpose:** A configured ElevenLabs AI agent linked to a tenant or clinic. Agents are not tied to a specific phone number or call direction. The same agent can serve WhatsApp chat, inbound calls, and outbound campaigns via different `phone_numbers` records.
895
+
896
+ | Column | Type | Required | Default | Description |
897
+ |--------|------|----------|---------|-------------|
898
+ | `agent_id` | UUID | Yes | `uuid()` | Primary key |
899
+ | `agent_tenant_id` | UUID | Yes | -- | FK to `tenants` |
900
+ | `agent_clinic_id` | UUID | No | `null` | FK to `clinics`. Null means tenant-level (shared across clinics) |
901
+ | `agent_elevenlabs_id` | String | Yes | -- | ElevenLabs agent ID (external identifier) |
902
+ | `agent_name` | String | Yes | `""` | Display name in the dashboard |
903
+ | `agent_description` | String | Yes | `""` | Description/notes |
904
+ | `agent_voice_id` | String | Yes | `""` | ElevenLabs voice ID for this agent |
905
+ | `agent_language` | String | Yes | `"tr"` | Agent language code |
906
+ | `agent_greeting` | String | Yes | `""` | First message for voice calls |
907
+ | `agent_system_prompt` | String | Yes | `""` | Custom system prompt override |
908
+ | `agent_ai_provider` | String | No | `null` | AI provider override. Null inherits from tenant default |
909
+ | `agent_ai_model` | String | No | `null` | AI model override. Null inherits from tenant/provider default |
910
+ | `agent_post_call_strategy` | PostCallStrategy | Yes | `summarize` | What to do after a call ends |
911
+ | `agent_post_call_wa_notify` | Boolean | Yes | `true` | Send WhatsApp notification with summary after call |
912
+ | `agent_post_call_sms_notify` | Boolean | Yes | `false` | Send SMS notification with summary after call |
913
+ | `agent_summary_prompt` | String | Yes | `""` | Custom prompt for AI summary generation |
914
+ | `agent_wa_message_template` | String | Yes | `""` | Custom WhatsApp notification template |
915
+ | `agent_extra_config` | Json | Yes | `"{}"` | Thin JSONB for truly dynamic edge-case config |
916
+ | `agent_is_active` | Boolean | Yes | `true` | Soft-disable toggle |
917
+ | `agent_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
918
+ | `agent_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
919
+
920
+ **Indexes:**
921
+
922
+ | Index | Columns | Purpose |
923
+ |-------|---------|---------|
924
+ | Primary | `agent_id` | PK lookup |
925
+ | `idx_agents_tenant` | `agent_tenant_id` | List all agents for a tenant |
926
+ | `idx_agents_clinic` | `agent_clinic_id` | List agents for a clinic |
927
+ | `idx_agents_elevenlabs` | `agent_elevenlabs_id` | Resolve ElevenLabs callbacks to our agent record |
928
+
929
+ **Relations:**
930
+
931
+ | Direction | Target | FK Column | Description |
932
+ |-----------|--------|-----------|-------------|
933
+ | belongs_to | tenants | `agent_tenant_id` | Parent tenant (cascade delete) |
934
+ | belongs_to | clinics | `agent_clinic_id` | Assigned clinic (optional) |
935
+ | has_many | phone_numbers | -- | Phone numbers routing to this agent |
936
+ | has_many | calls | -- | Calls handled by this agent |
937
+ | has_many | outbound_campaigns | -- | Campaigns using this agent |
938
+ | has_many | appointment_validation_batches | -- | Validation batches using this agent |
939
+
940
+ **Design decisions:**
941
+ - Voice and language config are top-level columns (not JSONB) because they are read on every call/session initiation.
942
+ - `agent_ai_provider` and `agent_ai_model` are nullable -- the resolution chain is: agent override > clinic default (`clinic_ai_provider`/`clinic_ai_model`) > system env var (`DEFAULT_AI_PROVIDER`).
943
+ - `agent_extra_config` JSONB is intentionally thin. If a field starts being queried frequently, it should be promoted to a column.
944
+
945
+ ---
946
+
947
+ ### 11. calls
948
+
949
+ **Purpose:** Records every voice call (inbound and outbound). Contains metadata and summary-level data. Heavy content (transcript, recording, analysis) is in a separate `call_details` table for performance.
950
+
951
+ | Column | Type | Required | Default | Description |
952
+ |--------|------|----------|---------|-------------|
953
+ | `call_id` | UUID | Yes | `uuid()` | Primary key |
954
+ | `call_tenant_id` | UUID | Yes | -- | FK to `tenants` |
955
+ | `call_clinic_id` | UUID | Yes | -- | FK to `clinics`. Resolved from phone number (inbound) or campaign/agent (outbound) |
956
+ | `call_patient_id` | UUID | No | `null` | FK to `patients`. Resolved by phone number match during post-call processing |
957
+ | `call_agent_id` | UUID | Yes | -- | FK to `agents`. Which agent handled the call |
958
+ | `call_campaign_id` | UUID | No | `null` | FK to `outbound_campaigns`. Non-null for campaign calls |
959
+ | `call_conversation_id` | String | No | `null` | ElevenLabs conversation ID |
960
+ | `call_direction` | CallDirection | Yes | -- | `inbound` or `outbound` |
961
+ | `call_caller_number` | String | Yes | -- | Who initiated the call (E.164) |
962
+ | `call_callee_number` | String | Yes | -- | Who was called (E.164) |
963
+ | `call_duration_seconds` | Int | Yes | `0` | Call duration in seconds |
964
+ | `call_start_time` | DateTime | No | `null` | When the call started |
965
+ | `call_end_time` | DateTime | No | `null` | When the call ended |
966
+ | `call_termination_reason` | String | No | `null` | Why the call ended (e.g., "user_hangup", "no_answer") |
967
+ | `call_cost` | Decimal | No | `null` | Calculated cost in TRY |
968
+ | `call_language` | String | No | `null` | Detected or configured language |
969
+ | `call_elevenlabs_status` | String | No | `null` | ElevenLabs-reported status |
970
+ | `call_status` | CallStatus | Yes | -- | Final call outcome |
971
+ | `call_sentiment` | Sentiment | No | `null` | AI-detected sentiment |
972
+ | `call_categories` | String[] | Yes | `[]` | AI-generated categories/tags |
973
+ | `call_wa_notification_sent` | Boolean | Yes | `false` | Whether post-call WhatsApp notification was sent |
974
+ | `call_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
975
+ | `call_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
976
+
977
+ **Indexes:**
978
+
979
+ | Index | Columns | Purpose |
980
+ |-------|---------|---------|
981
+ | Primary | `call_id` | PK lookup |
982
+ | `idx_calls_tenant_date` | `(call_tenant_id, call_created_at DESC)` | Paginated call list for a tenant (most recent first) |
983
+ | `idx_calls_clinic_date` | `(call_clinic_id, call_created_at DESC)` | Paginated call list for a clinic |
984
+ | `idx_calls_campaign_status` | `(call_campaign_id, call_status)` | Campaign results aggregation |
985
+ | `idx_calls_tenant_direction_date` | `(call_tenant_id, call_direction, call_created_at DESC)` | Filter by direction (inbound/outbound) |
986
+ | `idx_calls_conversation` | `call_conversation_id` | Resolve ElevenLabs webhook callbacks |
987
+ | `idx_calls_tenant_callee` | `(call_tenant_id, call_callee_number)` | Find calls to a specific phone number |
988
+ | `idx_calls_patient_date` | `(call_patient_id, call_created_at DESC)` | Patient call history (timeline) |
989
+
990
+ **Relations:**
991
+
992
+ | Direction | Target | FK Column | Description |
993
+ |-----------|--------|-----------|-------------|
994
+ | belongs_to | tenants | `call_tenant_id` | Parent tenant (cascade delete) |
995
+ | belongs_to | clinics | `call_clinic_id` | Assigned clinic |
996
+ | belongs_to | patients | `call_patient_id` | Patient (resolved by phone match post-call) |
997
+ | belongs_to | agents | `call_agent_id` | Agent that handled the call |
998
+ | belongs_to | outbound_campaigns | `call_campaign_id` | Parent campaign (if outbound campaign call) |
999
+ | has_one | call_details | -- | Detailed content (transcript, recording, analysis) |
1000
+ | has_many | billing_events | -- | Billing charges for this call |
1001
+ | has_many | leads | -- | Leads generated from this call |
1002
+ | has_many | outbound_campaign_entries | -- | Campaign entries linked to this call |
1003
+
1004
+ **Design decisions:**
1005
+ - Heavy content (transcript, recording URL, analysis JSON) is in a separate `call_details` table. This keeps the `calls` table lean for list queries where you only need metadata.
1006
+ - `call_clinic_id` is required. Every call belongs to a clinic — resolved from the phone number (inbound) or campaign/agent config (outbound).
1007
+ - `call_patient_id` is nullable. Resolved by matching caller/callee phone against `patients.patient_phone` during post-call processing. Null if no matching patient exists (e.g., unknown caller).
1008
+ - `Decimal` for `call_cost` to avoid floating-point rounding.
1009
+ - Eight indexes -- calls are the most-queried table in the system (dashboard, analytics, billing, patient timeline).
1010
+
1011
+ ---
1012
+
1013
+ ### 12. call_details
1014
+
1015
+ **Purpose:** Stores heavy content for a call (transcript, summary, recording, analysis). Loaded only when viewing a single call detail page. Separated from `calls` to keep list queries fast.
1016
+
1017
+ | Column | Type | Required | Default | Description |
1018
+ |--------|------|----------|---------|-------------|
1019
+ | `detail_id` | UUID | Yes | `uuid()` | Primary key |
1020
+ | `detail_call_id` | UUID | Yes (unique) | -- | FK to `calls`. One-to-one relationship |
1021
+ | `detail_transcript` | String | Yes | `""` | Full call transcript |
1022
+ | `detail_llm_summary` | String | No | `null` | AI-generated summary of the call |
1023
+ | `detail_extracted_data` | Json | No | `null` | AI-extracted structured data (varies by agent/campaign) |
1024
+ | `detail_recording_url` | String | No | `null` | External recording URL (ElevenLabs) |
1025
+ | `detail_recording_key` | String | No | `null` | MinIO object key if recording is mirrored locally |
1026
+ | `detail_analysis` | Json | No | `null` | ElevenLabs analysis payload |
1027
+ | `detail_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1028
+ | `detail_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1029
+
1030
+ **Indexes:**
1031
+
1032
+ | Index | Columns | Purpose |
1033
+ |-------|---------|---------|
1034
+ | Primary | `detail_id` | PK lookup |
1035
+ | Unique | `detail_call_id` | Enforce one-to-one with calls, fast join |
1036
+
1037
+ **Relations:**
1038
+
1039
+ | Direction | Target | FK Column | Description |
1040
+ |-----------|--------|-----------|-------------|
1041
+ | belongs_to | calls | `detail_call_id` | Parent call (cascade delete) |
1042
+
1043
+ **Design decisions:**
1044
+ - Transcript and summary are `String` (TEXT in PostgreSQL) -- no length limit. Call transcripts can be very long.
1045
+ - `detail_extracted_data` is JSONB because the structure varies per agent/campaign extraction schema.
1046
+ - `detail_recording_key` is for optional local mirroring of recordings to MinIO. If null, `detail_recording_url` points to the external provider.
1047
+
1048
+ ---
1049
+
1050
+ ### 13. leads
1051
+
1052
+ **Purpose:** Sales leads generated from calls and WhatsApp conversations. A lead represents a potential patient who expressed interest in a service.
1053
+
1054
+ | Column | Type | Required | Default | Description |
1055
+ |--------|------|----------|---------|-------------|
1056
+ | `lead_id` | UUID | Yes | `uuid()` | Primary key |
1057
+ | `lead_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1058
+ | `lead_clinic_id` | UUID | Yes | -- | FK to `clinics`. Resolved from source call or chat |
1059
+ | `lead_patient_id` | UUID | No | `null` | FK to `patients`. Linked when patient exists |
1060
+ | `lead_call_id` | UUID | No | `null` | FK to `calls`. Source: voice call |
1061
+ | `lead_session_id` | UUID | No | `null` | FK to `whatsapp_sessions`. Source: WhatsApp session |
1062
+ | `lead_chat_id` | UUID | No | `null` | FK to `whatsapp_chats`. Source: WhatsApp chat |
1063
+ | `lead_name` | String | No | `null` | Patient/lead name |
1064
+ | `lead_phone` | String | Yes | -- | Phone number (E.164) |
1065
+ | `lead_service_of_interest` | String | No | `null` | What service the lead is interested in |
1066
+ | `lead_status` | LeadStatus | Yes | `new_lead` | Current lifecycle stage |
1067
+ | `lead_categories` | String[] | Yes | `[]` | AI-generated categories |
1068
+ | `lead_summary` | String | No | `null` | AI-generated summary of the lead interaction |
1069
+ | `lead_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1070
+ | `lead_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1071
+
1072
+ **Indexes:**
1073
+
1074
+ | Index | Columns | Purpose |
1075
+ |-------|---------|---------|
1076
+ | Primary | `lead_id` | PK lookup |
1077
+ | `idx_leads_tenant_date` | `(lead_tenant_id, lead_created_at DESC)` | Paginated lead list |
1078
+ | `idx_leads_tenant_status` | `(lead_tenant_id, lead_status)` | Filter by status |
1079
+ | `idx_leads_call` | `lead_call_id` | Find lead by source call |
1080
+ | `idx_leads_session` | `lead_session_id` | Find lead by source session |
1081
+ | `idx_leads_patient` | `lead_patient_id` | Find leads for a patient (timeline) |
1082
+
1083
+ **Relations:**
1084
+
1085
+ | Direction | Target | FK Column | Description |
1086
+ |-----------|--------|-----------|-------------|
1087
+ | belongs_to | tenants | `lead_tenant_id` | Parent tenant (cascade delete) |
1088
+ | belongs_to | clinics | `lead_clinic_id` | Assigned clinic |
1089
+ | belongs_to | patients | `lead_patient_id` | Patient (linked when exists) |
1090
+ | belongs_to | calls | `lead_call_id` | Source call (nullable) |
1091
+ | belongs_to | whatsapp_sessions | `lead_session_id` | Source session (nullable) |
1092
+ | belongs_to | whatsapp_chats | `lead_chat_id` | Source chat (nullable) |
1093
+
1094
+ **Design decisions:**
1095
+ - Lead source is captured via nullable FKs (`lead_call_id`, `lead_session_id`) rather than a polymorphic `source_type`/`source_id` pattern. This enables proper foreign key constraints and direct joins.
1096
+ - A lead can have both `lead_call_id` and `lead_session_id` null (e.g., manually created), or one of them set.
1097
+ - `lead_categories` uses a `String[]` array rather than a junction table. Categories are AI-generated tags, not a controlled vocabulary.
1098
+
1099
+ ---
1100
+
1101
+ ### 14. outbound_campaigns
1102
+
1103
+ **Purpose:** Customizable outbound call campaigns. Each campaign defines its own agent configuration, extraction schema, and call schedule. Not tied to surveys -- fully flexible for any outbound calling use case (surveys, follow-ups, marketing, feedback collection).
1104
+
1105
+ | Column | Type | Required | Default | Description |
1106
+ |--------|------|----------|---------|-------------|
1107
+ | `campaign_id` | UUID | Yes | `uuid()` | Primary key |
1108
+ | `campaign_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1109
+ | `campaign_clinic_id` | UUID | Yes | -- | FK to `clinics`. Which branch runs this campaign |
1110
+ | `campaign_agent_id` | UUID | Yes | -- | FK to `agents`. Agent used for calls |
1111
+ | `campaign_name` | String | Yes | -- | Campaign name |
1112
+ | `campaign_description` | String | Yes | `""` | Campaign description |
1113
+ | `campaign_status` | OutboundCampaignStatus | Yes | `draft` | Current lifecycle state |
1114
+ | `campaign_agent_config` | Json | Yes | `"{}"` | Agent config: `{prompt, first_message, language, voice_id, extraction_schema, post_call_prompt}` |
1115
+ | `campaign_schedule` | Json | Yes | -- | Schedule: `{start_date, end_date, daily_start_time, daily_end_time, days_of_week, max_concurrent, max_retries, retry_delay_minutes}` |
1116
+ | `campaign_phone_id` | UUID | No | `null` | FK to `phone_numbers`. Outbound phone number |
1117
+ | `campaign_stats` | Json | Yes | `"{}"` | Aggregated stats: `{total, pending, calling, completed, failed, no_answer, voicemail}` |
1118
+ | `campaign_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1119
+ | `campaign_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1120
+
1121
+ **Indexes:**
1122
+
1123
+ | Index | Columns | Purpose |
1124
+ |-------|---------|---------|
1125
+ | Primary | `campaign_id` | PK lookup |
1126
+ | `idx_campaigns_tenant_status` | `(campaign_tenant_id, campaign_status)` | List campaigns filtered by status |
1127
+ | `idx_campaigns_clinic` | `campaign_clinic_id` | List campaigns for a clinic |
1128
+
1129
+ **Relations:**
1130
+
1131
+ | Direction | Target | FK Column | Description |
1132
+ |-----------|--------|-----------|-------------|
1133
+ | belongs_to | tenants | `campaign_tenant_id` | Parent tenant (cascade delete) |
1134
+ | belongs_to | clinics | `campaign_clinic_id` | Scoped clinic |
1135
+ | belongs_to | agents | `campaign_agent_id` | Agent handling calls |
1136
+ | belongs_to | phone_numbers | `campaign_phone_id` | Outbound phone number |
1137
+ | has_many | outbound_campaign_entries | -- | Phone list entries |
1138
+ | has_many | calls | -- | Calls made for this campaign |
1139
+
1140
+ **Design decisions:**
1141
+ - `campaign_agent_config` is JSONB because it contains a complex, campaign-specific configuration structure (prompt, extraction schema, etc.) that varies per campaign.
1142
+ - `campaign_stats` is a denormalized aggregate stored as JSONB. Updated after each call completes. Avoids expensive COUNT queries on the entries table for dashboard rendering.
1143
+ - State machine: `draft` -> `scheduled` -> `running` <-> `paused` -> `completed`. Only `draft` campaigns can be edited or deleted.
1144
+
1145
+ ---
1146
+
1147
+ ### 15. outbound_campaign_entries
1148
+
1149
+ **Purpose:** Individual phone list entries within an outbound campaign. Each entry is one person to call, with their custom data and call results.
1150
+
1151
+ | Column | Type | Required | Default | Description |
1152
+ |--------|------|----------|---------|-------------|
1153
+ | `centry_id` | UUID | Yes | `uuid()` | Primary key |
1154
+ | `centry_campaign_id` | UUID | Yes | -- | FK to `outbound_campaigns` |
1155
+ | `centry_phone` | String | Yes | -- | Phone number to call (E.164) |
1156
+ | `centry_contact_name` | String | Yes | `""` | Contact name |
1157
+ | `centry_custom_data` | Json | Yes | `"{}"` | Per-entry dynamic variables passed to agent (e.g., patient name, product) |
1158
+ | `centry_status` | CampaignEntryStatus | Yes | `pending` | Current call status |
1159
+ | `centry_attempts` | Int | Yes | `0` | Number of call attempts made |
1160
+ | `centry_max_attempts` | Int | Yes | `3` | Maximum retry attempts |
1161
+ | `centry_last_attempt_at` | DateTime | No | `null` | Timestamp of last call attempt |
1162
+ | `centry_conversation_id` | String | No | `null` | ElevenLabs conversation ID for the call |
1163
+ | `centry_call_id` | UUID | No | `null` | FK to `calls`. Linked after call completes |
1164
+ | `centry_call_duration_secs` | Int | Yes | `0` | Duration of the call |
1165
+ | `centry_extracted_data` | Json | No | `null` | AI-extracted data per campaign extraction_schema |
1166
+ | `centry_summary` | String | No | `null` | AI-generated call summary |
1167
+ | `centry_called_at` | DateTime | No | `null` | When the first call was made |
1168
+ | `centry_completed_at` | DateTime | No | `null` | When the entry reached terminal status |
1169
+ | `centry_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1170
+ | `centry_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1171
+
1172
+ **Indexes:**
1173
+
1174
+ | Index | Columns | Purpose |
1175
+ |-------|---------|---------|
1176
+ | Primary | `centry_id` | PK lookup |
1177
+ | `idx_centries_campaign_status` | `(centry_campaign_id, centry_status)` | Dispatch next entries, count by status |
1178
+ | `idx_centries_conversation` | `centry_conversation_id` | Resolve ElevenLabs post-call webhook to entry |
1179
+
1180
+ **Relations:**
1181
+
1182
+ | Direction | Target | FK Column | Description |
1183
+ |-----------|--------|-----------|-------------|
1184
+ | belongs_to | outbound_campaigns | `centry_campaign_id` | Parent campaign (cascade delete) |
1185
+ | belongs_to | calls | `centry_call_id` | Linked call record |
1186
+
1187
+ **Design decisions:**
1188
+ - `centry_custom_data` is JSONB because it is tenant-defined, varying per campaign. Passed to the AI agent as dynamic variables.
1189
+ - `centry_extracted_data` is JSONB because its structure is defined by the campaign's `extraction_schema`.
1190
+ - Cascade delete from campaign: deleting a campaign deletes all its entries.
1191
+
1192
+ ---
1193
+
1194
+ ### 16. whatsapp_chats
1195
+
1196
+ **Purpose:** Represents a WhatsApp (or Instagram) conversation thread with a patient. Each unique patient-phone-number combination gets one chat record. A chat can have multiple sessions over time.
1197
+
1198
+ | Column | Type | Required | Default | Description |
1199
+ |--------|------|----------|---------|-------------|
1200
+ | `chat_id` | UUID | Yes | `uuid()` | Primary key |
1201
+ | `chat_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1202
+ | `chat_clinic_id` | UUID | Yes | -- | FK to `clinics`. Resolved from receiving phone number |
1203
+ | `chat_patient_id` | UUID | No | `null` | FK to `patients`. Linked on first message (auto-created or matched by phone) |
1204
+ | `chat_channel` | ChatChannel | Yes | `whatsapp` | `whatsapp` or `instagram` |
1205
+ | `chat_external_id` | String | Yes | -- | Provider chat identifier (Meta conversation ID). Treated as opaque string |
1206
+ | `chat_phone_id` | UUID | No | `null` | FK to `phone_numbers`. Which of our numbers received this chat |
1207
+ | `chat_contact_name` | String | Yes | `"Bilinmiyor"` | Contact display name from WhatsApp/Instagram profile |
1208
+ | `chat_contact_phone` | String | Yes | `""` | Contact phone number (denormalized from patient) |
1209
+ | `chat_is_closed` | Boolean | Yes | `false` | Whether the chat is manually closed/archived |
1210
+ | `chat_active_session_id` | UUID | No | `null` | Currently active session (denormalized for fast lookup) |
1211
+ | `chat_last_message_at` | DateTime | Yes | `now()` | Timestamp of most recent message (for sorting) |
1212
+ | `chat_last_message_preview` | String | Yes | `""` | Preview text of the last message (for chat list UI) |
1213
+ | `chat_message_count` | Int | Yes | `0` | Total message count (denormalized) |
1214
+ | `chat_deleted_at` | DateTime | No | `null` | Soft-delete timestamp |
1215
+ | `chat_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1216
+ | `chat_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1217
+
1218
+ **Indexes:**
1219
+
1220
+ | Index | Columns | Purpose |
1221
+ |-------|---------|---------|
1222
+ | Primary | `chat_id` | PK lookup |
1223
+ | Unique | `(chat_tenant_id, chat_external_id)` | Prevent duplicate chats for the same external conversation |
1224
+ | `idx_chats_tenant_clinic` | `(chat_tenant_id, chat_clinic_id)` | List chats for a clinic |
1225
+ | `idx_chats_tenant_lastmsg` | `(chat_tenant_id, chat_last_message_at DESC)` | Chat list sorted by most recent message |
1226
+ | `idx_chats_tenant_phone` | `(chat_tenant_id, chat_contact_phone)` | Find chat by patient phone number |
1227
+
1228
+ **Relations:**
1229
+
1230
+ | Direction | Target | FK Column | Description |
1231
+ |-----------|--------|-----------|-------------|
1232
+ | belongs_to | tenants | `chat_tenant_id` | Parent tenant (cascade delete) |
1233
+ | belongs_to | clinics | `chat_clinic_id` | Assigned clinic |
1234
+ | belongs_to | patients | `chat_patient_id` | Patient (linked on first message) |
1235
+ | belongs_to | phone_numbers | `chat_phone_id` | Receiving phone number |
1236
+ | has_many | whatsapp_sessions | -- | Sessions within this chat |
1237
+ | has_many | whatsapp_messages | -- | All messages |
1238
+ | has_many | operator_requests | -- | Operator requests from this chat |
1239
+ | has_many | appointments | -- | Appointments booked in this chat |
1240
+ | has_many | leads | -- | Leads generated from this chat |
1241
+
1242
+ **Design decisions:**
1243
+ - `chat_contact_id` replaced by `chat_patient_id`. Links to `patients` table instead of the removed `whatsapp_contact_profiles`.
1244
+ - `chat_external_account_id` dropped — was a legacy field for backward compatibility. Clean rewrite doesn't need it.
1245
+ - `chat_clinic_id` is required. Resolved from the receiving phone number on first message.
1246
+ - Denormalized fields (`chat_last_message_at`, `chat_last_message_preview`, `chat_message_count`) are updated on every new message to avoid expensive aggregation queries on the chat list page.
1247
+ - `chat_active_session_id` is denormalized to avoid a JOIN with sessions on every incoming message check.
1248
+ - `chat_external_id` is the Meta conversation ID. Treated as an opaque string.
1249
+
1250
+ ---
1251
+
1252
+ ### 17. whatsapp_sessions
1253
+
1254
+ **Purpose:** A session represents a single conversation interaction within a chat. When a patient sends a message after a period of inactivity, a new session is created. Sessions go through a lifecycle: `waiting` (grace period) -> `active` (AI responding) -> `resolved` (ended).
1255
+
1256
+ | Column | Type | Required | Default | Description |
1257
+ |--------|------|----------|---------|-------------|
1258
+ | `session_id` | UUID | Yes | `uuid()` | Primary key |
1259
+ | `session_chat_id` | UUID | Yes | -- | FK to `whatsapp_chats` |
1260
+ | `session_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1261
+ | `session_clinic_id` | UUID | Yes | -- | FK to `clinics`. Inherited from parent chat |
1262
+ | `session_status` | SessionStatus | Yes | `waiting` | Current lifecycle state |
1263
+ | `session_resolved_by` | SessionResolvedBy | No | `null` | How the session was resolved (null while active) |
1264
+ | `session_started_at` | DateTime | Yes | `now()` | When the session started |
1265
+ | `session_resolved_at` | DateTime | No | `null` | When the session was resolved |
1266
+ | `session_grace_deadline` | DateTime | No | `null` | When the grace period expires (BullMQ delayed job fires at this time) |
1267
+ | `session_taken_over_by_id` | UUID | No | `null` | FK to `users`. Staff member who took over from AI |
1268
+ | `session_taken_over_at` | DateTime | No | `null` | When the takeover happened |
1269
+ | `session_el_conversation_id` | String | No | `null` | Current ElevenLabs conversation ID |
1270
+ | `session_el_prev_conversation_ids` | String[] | Yes | `[]` | Previous conversation IDs (after reconnects) |
1271
+ | `session_el_conversation_created` | DateTime | No | `null` | When the ElevenLabs conversation was created |
1272
+ | `session_el_last_interaction` | DateTime | No | `null` | Last message exchange with ElevenLabs |
1273
+ | `session_el_cost` | Decimal | No | `null` | ElevenLabs cost for this session (fetched async) |
1274
+ | `session_ref_code` | String | No | `null` | Operator request REF code (e.g., `R-AB2X`) |
1275
+ | `session_awaiting_operator` | Boolean | Yes | `false` | True when waiting for doctor response. Prevents AI from auto-resolving |
1276
+ | `session_message_count` | Int | Yes | `0` | Messages in this session |
1277
+ | `session_llm_summary` | String | No | `null` | AI-generated session summary |
1278
+ | `session_sentiment` | Sentiment | No | `null` | AI-detected sentiment |
1279
+ | `session_categories` | String[] | Yes | `[]` | AI-generated categories |
1280
+ | `session_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1281
+ | `session_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1282
+
1283
+ **Indexes:**
1284
+
1285
+ | Index | Columns | Purpose |
1286
+ |-------|---------|---------|
1287
+ | Primary | `session_id` | PK lookup |
1288
+ | `idx_sessions_chat_status` | `(session_chat_id, session_status)` | Find active session for a chat (every incoming message) |
1289
+ | `idx_sessions_status_grace` | `(session_status, session_grace_deadline)` | Grace period expiry sweep |
1290
+ | `idx_sessions_tenant_status` | `(session_tenant_id, session_status)` | List active sessions for a tenant |
1291
+ | `idx_sessions_clinic_status` | `(session_clinic_id, session_status)` | List active sessions for a clinic |
1292
+ | `idx_sessions_status_updated` | `(session_status, session_updated_at)` | Inactivity sweep (resolve sessions idle > 2 hours) |
1293
+ | `idx_sessions_ref_tenant` | `(session_ref_code, session_tenant_id)` | Operator response matching: find session by REF code |
1294
+
1295
+ **Relations:**
1296
+
1297
+ | Direction | Target | FK Column | Description |
1298
+ |-----------|--------|-----------|-------------|
1299
+ | belongs_to | tenants | `session_tenant_id` | Parent tenant (cascade delete) |
1300
+ | belongs_to | clinics | `session_clinic_id` | Assigned clinic |
1301
+ | belongs_to | whatsapp_chats | `session_chat_id` | Parent chat (cascade delete) |
1302
+ | belongs_to | users (TakenOverBy) | `session_taken_over_by_id` | Staff who took over |
1303
+ | has_many | whatsapp_messages | -- | Messages in this session |
1304
+ | has_many | operator_requests | -- | Operator requests in this session |
1305
+ | has_many | appointments | -- | Appointments booked during this session |
1306
+ | has_many | leads | -- | Leads generated from this session |
1307
+
1308
+ **Design decisions:**
1309
+ - This is the most index-heavy table because sessions are queried from multiple angles: by chat (incoming message routing), by status (sweeps), by tenant/clinic (dashboard), by REF code (operator responses).
1310
+ - `session_el_prev_conversation_ids` tracks previous ElevenLabs conversations when WebSocket reconnections happen (chat continuity feature). Stored as an array on the session rather than a separate table because the list is short and always loaded with the session.
1311
+ - `session_awaiting_operator` is a boolean flag that prevents the inactivity sweep from resolving a session while the doctor has not yet responded.
1312
+
1313
+ ---
1314
+
1315
+ ### 18. whatsapp_messages
1316
+
1317
+ **Purpose:** Individual messages within a WhatsApp or Instagram conversation. Each message belongs to a chat and optionally to a session. Message text is **encrypted at rest** (AES-256-GCM).
1318
+
1319
+ | Column | Type | Required | Default | Description |
1320
+ |--------|------|----------|---------|-------------|
1321
+ | `msg_id` | UUID | Yes | `uuid()` | Primary key |
1322
+ | `msg_chat_id` | UUID | Yes | -- | FK to `whatsapp_chats` |
1323
+ | `msg_session_id` | UUID | No | `null` | FK to `whatsapp_sessions`. Null for messages outside a session |
1324
+ | `msg_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1325
+ | `msg_external_id` | String | No | `null` | Provider message ID (Meta) for deduplication |
1326
+ | `msg_sender` | MessageSender | Yes | -- | `contact`, `ai`, `human`, or `system` |
1327
+ | `msg_sender_name` | String | Yes | `""` | Display name of the sender |
1328
+ | `msg_sender_user_id` | UUID | No | `null` | FK to `users`. Populated when `sender = 'human'` |
1329
+ | `msg_text` | String | Yes | -- | **Encrypted.** AES-256-GCM via `MESSAGE_ENCRYPTION_KEY`. Stored as base64(iv + authTag + ciphertext) |
1330
+ | `msg_media_type` | String | No | `null` | MIME-like type (e.g., `imageMessage`, `audioMessage`, `documentMessage`). Not encrypted |
1331
+ | `msg_media_key` | String | No | `null` | MinIO object key for media files. Not encrypted (key is opaque, file is SSE-encrypted in MinIO) |
1332
+ | `msg_ad_context` | Json | No | `null` | Ad attribution: `{ad: {title, body, source_url}, source, app}` |
1333
+ | `msg_sent_via_bridge` | Boolean | Yes | `false` | Whether the message was sent through Meta Business API |
1334
+ | `msg_quoted_text` | String | No | `null` | **Encrypted.** Text of the quoted/replied-to message. Same encryption as `msg_text` |
1335
+ | `msg_created_at` | DateTime | Yes | `now()` | Message timestamp |
1336
+
1337
+ **Indexes:**
1338
+
1339
+ | Index | Columns | Purpose |
1340
+ |-------|---------|---------|
1341
+ | Primary | `msg_id` | PK lookup |
1342
+ | `idx_msgs_chat_date` | `(msg_chat_id, msg_created_at)` | Load messages for a chat (ascending, paginated) |
1343
+ | `idx_msgs_session_date` | `(msg_session_id, msg_created_at)` | Load messages for a session |
1344
+ | `idx_msgs_tenant_date` | `(msg_tenant_id, msg_created_at DESC)` | Message listing/export |
1345
+
1346
+ **Relations:**
1347
+
1348
+ | Direction | Target | FK Column | Description |
1349
+ |-----------|--------|-----------|-------------|
1350
+ | belongs_to | tenants | `msg_tenant_id` | Parent tenant (cascade delete) |
1351
+ | belongs_to | whatsapp_chats | `msg_chat_id` | Parent chat (cascade delete) |
1352
+ | belongs_to | whatsapp_sessions | `msg_session_id` | Parent session (optional) |
1353
+ | belongs_to | users | `msg_sender_user_id` | Human sender (when `sender = 'human'`) |
1354
+
1355
+ **Design decisions:**
1356
+ - **Message text is encrypted at rest.** `msg_text` and `msg_quoted_text` are AES-256-GCM encrypted using `MESSAGE_ENCRYPTION_KEY` (32-byte hex env var). Application encrypts before write, decrypts on read. Database and backups contain only opaque ciphertext.
1357
+ - **No full-text search on message content.** Encrypted text cannot be searched at the SQL level. With 7-day message retention (`clinic_message_retention_days`), deep search over history is not needed.
1358
+ - **Metadata is NOT encrypted:** `msg_sender`, `msg_media_type`, `msg_created_at` remain in plaintext for filtering, sorting, and indexing.
1359
+ - No `msg_updated_at` column. Messages are immutable once created.
1360
+ - `msg_media_key` stores the MinIO object key. Media files are SSE-encrypted in MinIO separately.
1361
+ - `msg_ad_context` captures WhatsApp ad attribution data. JSONB because the structure comes from Meta and may evolve.
1362
+ - `msg_session_id` is nullable because messages can arrive between sessions.
1363
+ - **Auto-deletion:** BullMQ job `whatsapp:message-cleanup` runs daily, deletes messages older than `clinic.clinic_message_retention_days` (default 7). Also removes associated MinIO media files.
1364
+
1365
+ ---
1366
+
1367
+ ### 19. ~~whatsapp_contact_profiles~~ — REMOVED
1368
+
1369
+ > **This table has been removed.** Replaced by the `patients` table (#36). All contact profile data (phone, name, tags, AI summary, language, interaction stats) now lives on the Patient model. See [19-PATIENTS.md](19-PATIENTS.md) for migration details.
1370
+
1371
+ ---
1372
+
1373
+ ### 20. operator_requests
1374
+
1375
+ **Purpose:** Tracks operator-in-the-loop requests. The AI forwards a message (text, media, or both) to a doctor/operator for review. The doctor responds via WhatsApp with the REF code. Media is optional — requests can be text-only (e.g., forwarding a patient question) or include photos/videos.
1376
+
1377
+ | Column | Type | Required | Default | Description |
1378
+ |--------|------|----------|---------|-------------|
1379
+ | `opreq_id` | UUID | Yes | `uuid()` | Primary key |
1380
+ | `opreq_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1381
+ | `opreq_clinic_id` | UUID | Yes | -- | FK to `clinics` |
1382
+ | `opreq_session_id` | UUID | Yes | -- | FK to `whatsapp_sessions` |
1383
+ | `opreq_chat_id` | UUID | Yes | -- | FK to `whatsapp_chats` |
1384
+ | `opreq_ref_code` | String | Yes | -- | Reference code (e.g., `R-AB2X`). Unique per tenant |
1385
+ | `opreq_request_type` | String | Yes | `"general"` | Type: `general`, `photo_review`, `conversation_forward`, or custom |
1386
+ | `opreq_request_message` | String | No | `null` | **Encrypted.** Text/summary forwarded to the doctor. AES-256-GCM via `MESSAGE_ENCRYPTION_KEY` |
1387
+ | `opreq_status` | OperatorRequestStatus | Yes | `pending` | `pending` → `responded` / `expired` / `cancelled` |
1388
+ | `opreq_forwarded_media_types` | String[] | Yes | `[]` | Types of media forwarded. Empty for text-only requests |
1389
+ | `opreq_forwarded_count` | Int | Yes | `0` | Number of media items successfully forwarded. 0 for text-only |
1390
+ | `opreq_failed_count` | Int | Yes | `0` | Number of media items that failed to forward |
1391
+ | `opreq_forwarded_at` | DateTime | Yes | `now()` | When request was forwarded to doctor |
1392
+ | `opreq_forwarded_to_phone` | String | Yes | -- | Doctor's phone number |
1393
+ | `opreq_operator_response` | String | No | `null` | **Encrypted.** Doctor's reply text. AES-256-GCM via `MESSAGE_ENCRYPTION_KEY` |
1394
+ | `opreq_response_media_type` | String | No | `null` | Media type if doctor replied with a file/image |
1395
+ | `opreq_response_message_id` | String | No | `null` | Provider message ID of the doctor's reply |
1396
+ | `opreq_responded_at` | DateTime | No | `null` | When the doctor responded |
1397
+ | `opreq_patient_message_ids` | String[] | Yes | `[]` | Provider message IDs of patient messages that were forwarded |
1398
+ | `opreq_expires_at` | DateTime | Yes | -- | When this request expires if unanswered |
1399
+ | `opreq_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1400
+ | `opreq_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1401
+
1402
+ **Indexes:**
1403
+
1404
+ | Index | Columns | Purpose |
1405
+ |-------|---------|---------|
1406
+ | Primary | `opreq_id` | PK lookup |
1407
+ | Unique | `(opreq_tenant_id, opreq_ref_code)` | REF code uniqueness within a tenant |
1408
+ | `idx_opreqs_session_status` | `(opreq_session_id, opreq_status)` | Find pending request for a session |
1409
+ | `idx_opreqs_status_expires` | `(opreq_status, opreq_expires_at)` | Expiry sweep (BullMQ every 15 min) |
1410
+
1411
+ **Relations:**
1412
+
1413
+ | Direction | Target | FK Column | Description |
1414
+ |-----------|--------|-----------|-------------|
1415
+ | belongs_to | tenants | `opreq_tenant_id` | Parent tenant (cascade delete) |
1416
+ | belongs_to | clinics | `opreq_clinic_id` | Assigned clinic |
1417
+ | belongs_to | whatsapp_sessions | `opreq_session_id` | Session that initiated the request |
1418
+ | belongs_to | whatsapp_chats | `opreq_chat_id` | Chat the patient is in |
1419
+
1420
+ **Design decisions:**
1421
+ - **Generalized for any request type.** `opreq_request_type` defaults to `general`. Photo review (`photo_review`) and conversation forwarding (`conversation_forward`) are specific types. Media fields are empty/zero for text-only requests.
1422
+ - **Text fields encrypted.** `opreq_request_message` and `opreq_operator_response` contain patient-related medical/pricing info. Encrypted with `MESSAGE_ENCRYPTION_KEY` (same key as chat messages).
1423
+ - `opreq_clinic_id` is required. The request always belongs to a clinic (inherited from the session's clinic).
1424
+ - REF code format: `{prefix}-{4 alphanumeric}` (e.g., `R-AB2X`). Unique per tenant. Prefix configurable per clinic.
1425
+ - `opreq_forwarded_media_types` is an array tracking each forwarded media item's type. Empty array = text-only request.
1426
+ - `opreq_patient_message_ids` stores provider message IDs for media retrieval. Empty for text-only requests.
1427
+
1428
+ ---
1429
+
1430
+ ### 21. appointments
1431
+
1432
+ **Purpose:** The source of truth for all appointments. Booked via AI (WhatsApp, voice call), manually by staff from the dashboard, or as walk-ins. Google Calendar is an optional sync target — not the source of truth.
1433
+
1434
+ | Column | Type | Required | Default | Description |
1435
+ |--------|------|----------|---------|-------------|
1436
+ | `appt_id` | UUID | Yes | `uuid()` | Primary key |
1437
+ | `appt_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1438
+ | `appt_clinic_id` | UUID | Yes | -- | FK to `clinics` |
1439
+ | `appt_patient_id` | UUID | Yes | -- | FK to `patients`. Every appointment belongs to a patient |
1440
+ | `appt_doctor_id` | UUID | No | `null` | FK to `users` (doctor role). Which doctor performs the treatment |
1441
+ | `appt_treatment_id` | UUID | No | `null` | FK to `treatments`. What treatment this is for |
1442
+ | `appt_treatment_plan_item_id` | UUID | No | `null` | FK to `treatment_plan_items`. Which plan item (`tpitem_id`), if part of a plan |
1443
+ | `appt_chat_id` | UUID | No | `null` | FK to `whatsapp_chats`. Null for voice, dashboard, walk-in |
1444
+ | `appt_session_id` | UUID | No | `null` | FK to `whatsapp_sessions`. Null for voice, dashboard, walk-in |
1445
+ | `appt_google_event_id` | String | No | `null` | Google Calendar event ID. Null if Google Calendar not connected or not synced |
1446
+ | `appt_patient_name` | String | Yes | -- | Patient name (denormalized for list display) |
1447
+ | `appt_patient_phone` | String | Yes | -- | Patient phone (denormalized) |
1448
+ | `appt_start_time` | DateTime | Yes | -- | Appointment start time |
1449
+ | `appt_end_time` | DateTime | Yes | -- | Appointment end time |
1450
+ | `appt_duration_minutes` | Int | Yes | -- | Duration in minutes |
1451
+ | `appt_type` | AppointmentType | Yes | `appointment` | `appointment`, `walk_in`, `follow_up`, `consultation` |
1452
+ | `appt_status` | AppointmentStatus | Yes | `scheduled` | `scheduled`, `confirmed`, `in_progress`, `completed`, `cancelled`, `no_show` |
1453
+ | `appt_source` | AppointmentSource | Yes | `dashboard` | `whatsapp`, `voice_call`, `dashboard`, `walk_in` |
1454
+ | `appt_notes` | String | Yes | `""` | Appointment notes |
1455
+ | `appt_conversation_id` | String | No | `null` | ElevenLabs conversation ID (for AI-booked) |
1456
+ | `appt_cancelled_at` | DateTime | No | `null` | When the appointment was cancelled |
1457
+ | `appt_completed_at` | DateTime | No | `null` | When the appointment was marked completed |
1458
+ | `appt_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1459
+ | `appt_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1460
+
1461
+ **Indexes:**
1462
+
1463
+ | Index | Columns | Purpose |
1464
+ |-------|---------|---------|
1465
+ | Primary | `appt_id` | PK lookup |
1466
+ | `idx_appts_tenant_start` | `(appt_tenant_id, appt_start_time)` | Appointment list sorted by date |
1467
+ | `idx_appts_clinic_start` | `(appt_clinic_id, appt_start_time)` | Clinic-scoped appointment list |
1468
+ | `idx_appts_patient_start` | `(appt_patient_id, appt_start_time)` | Patient appointment history (timeline) |
1469
+ | `idx_appts_doctor_start` | `(appt_doctor_id, appt_start_time)` | Doctor's schedule |
1470
+ | `idx_appts_tenant_status` | `(appt_tenant_id, appt_status)` | Filter by status |
1471
+ | `idx_appts_google_event` | `(appt_google_event_id)` | Google Calendar sync lookup. Partial index on non-null values |
1472
+
1473
+ **Relations:**
1474
+
1475
+ | Direction | Target | FK Column | Description |
1476
+ |-----------|--------|-----------|-------------|
1477
+ | belongs_to | tenants | `appt_tenant_id` | Parent tenant (cascade delete) |
1478
+ | belongs_to | clinics | `appt_clinic_id` | Which clinic's calendar |
1479
+ | belongs_to | patients | `appt_patient_id` | The patient |
1480
+ | belongs_to | users (doctor) | `appt_doctor_id` | Assigned doctor |
1481
+ | belongs_to | treatments | `appt_treatment_id` | Treatment type |
1482
+ | belongs_to | treatment_plan_items | `appt_treatment_plan_item_id` | Plan item (if part of a plan) |
1483
+ | belongs_to | whatsapp_chats | `appt_chat_id` | Source chat (nullable) |
1484
+ | belongs_to | whatsapp_sessions | `appt_session_id` | Source session (nullable) |
1485
+
1486
+ **Design decisions:**
1487
+ - **Our system is the source of truth.** Appointments are created and managed here. Google Calendar is an optional integration — if connected, appointments are synced to Google Calendar. If not connected, appointments still work fully.
1488
+ - `appt_google_event_id` is nullable. Walk-ins, manual bookings, and clinics without Google Calendar have no event ID. When Google Calendar is connected, the event ID is stored after sync for two-way updates.
1489
+ - `appt_patient_id` is required. Every appointment belongs to a patient. The patient is resolved by phone number (for AI bookings) or selected from the dashboard (for manual bookings).
1490
+ - `appt_doctor_id` is nullable. Some appointments don't have a specific doctor (e.g., general consultation at a small clinic where any available doctor sees the patient).
1491
+ - `appt_treatment_id` is nullable. Consultations and walk-ins may not have a specific treatment.
1492
+ - `appt_treatment_plan_item_id` links the appointment to a specific step in a treatment plan. When the appointment completes, the plan item status updates to `completed`.
1493
+ - `appt_patient_name` and `appt_patient_phone` are denormalized from the patient record for list display without JOIN.
1494
+ - `appt_completed_at` added to track when a visit actually finished (separate from `updated_at` which changes on any edit).
1495
+ - The old unique constraint on `(google_event_id, tenant_id)` is replaced by a non-unique index since `google_event_id` is now nullable.
1496
+
1497
+ ---
1498
+
1499
+ ### 22. appointment_validation_batches
1500
+
1501
+ **Purpose:** A batch of outbound calls to validate upcoming appointments. Staff uploads a CSV of appointments, the system calls each patient to confirm, cancel, or reschedule.
1502
+
1503
+ | Column | Type | Required | Default | Description |
1504
+ |--------|------|----------|---------|-------------|
1505
+ | `batch_id` | UUID | Yes | `uuid()` | Primary key |
1506
+ | `batch_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1507
+ | `batch_clinic_id` | UUID | Yes | -- | FK to `clinics`. Which branch runs this validation |
1508
+ | `batch_created_by_id` | UUID | Yes | -- | FK to `users`. Who created this batch |
1509
+ | `batch_name` | String | Yes | -- | Batch name (e.g., "Monday Appointments") |
1510
+ | `batch_status` | BatchStatus | Yes | `draft` | Current lifecycle state |
1511
+ | `batch_el_batch_id` | String | No | `null` | ElevenLabs batch call ID |
1512
+ | `batch_agent_id` | UUID | No | `null` | FK to `agents`. Agent used for validation calls |
1513
+ | `batch_phone_id` | UUID | No | `null` | FK to `phone_numbers`. Outbound phone number |
1514
+ | `batch_column_mapping` | Json | Yes | -- | Maps CSV columns to fields: `{patient_name, patient_phone, appointment_date, ...}` |
1515
+ | `batch_call_schedule` | Json | Yes | -- | Call timing: `{call_date, call_start_time, call_end_time, max_concurrent, ...}` |
1516
+ | `batch_stats` | Json | Yes | `"{}"` | Aggregated stats (denormalized for dashboard) |
1517
+ | `batch_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1518
+ | `batch_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1519
+
1520
+ **Indexes:**
1521
+
1522
+ | Index | Columns | Purpose |
1523
+ |-------|---------|---------|
1524
+ | Primary | `batch_id` | PK lookup |
1525
+ | `idx_batches_tenant_date` | `(batch_tenant_id, batch_created_at DESC)` | Paginated batch list |
1526
+ | `idx_batches_el` | `batch_el_batch_id` | Resolve ElevenLabs callbacks |
1527
+ | `idx_batches_clinic` | `batch_clinic_id` | List batches for a clinic |
1528
+
1529
+ **Relations:**
1530
+
1531
+ | Direction | Target | FK Column | Description |
1532
+ |-----------|--------|-----------|-------------|
1533
+ | belongs_to | tenants | `batch_tenant_id` | Parent tenant (cascade delete) |
1534
+ | belongs_to | clinics | `batch_clinic_id` | Scoped clinic |
1535
+ | belongs_to | users (CreatedBy) | `batch_created_by_id` | Creator |
1536
+ | belongs_to | agents | `batch_agent_id` | Validation agent |
1537
+ | belongs_to | phone_numbers | `batch_phone_id` | Outbound phone number |
1538
+ | has_many | appointment_validation_entries | -- | Individual entries |
1539
+
1540
+ **Design decisions:**
1541
+ - `batch_column_mapping` is JSONB because the CSV column structure varies per upload.
1542
+ - `batch_stats` is denormalized (updated after each entry status change) to avoid expensive aggregation on the dashboard.
1543
+ - Lifecycle: `draft` -> `submitted` -> `in_progress` -> `completed` or `cancelled`.
1544
+
1545
+ ---
1546
+
1547
+ ### 23. appointment_validation_entries
1548
+
1549
+ **Purpose:** Individual rows from the uploaded CSV. Each entry is one patient to call for appointment validation.
1550
+
1551
+ | Column | Type | Required | Default | Description |
1552
+ |--------|------|----------|---------|-------------|
1553
+ | `bentry_id` | UUID | Yes | `uuid()` | Primary key |
1554
+ | `bentry_batch_id` | UUID | Yes | -- | FK to `appointment_validation_batches` |
1555
+ | `bentry_appointment_id` | UUID | No | `null` | FK to `appointments`. Linked when validation entry matches an appointment in our system |
1556
+ | `bentry_row_index` | Int | Yes | -- | Original row number from CSV (for display/ordering) |
1557
+ | `bentry_external_id` | String | Yes | `""` | External appointment ID from the source system |
1558
+ | `bentry_patient_name` | String | Yes | -- | Patient name |
1559
+ | `bentry_patient_phone` | String | Yes | -- | Patient phone (E.164) |
1560
+ | `bentry_appointment_date` | String | Yes | -- | Appointment date as string (from CSV, not parsed to DateTime) |
1561
+ | `bentry_appointment_time` | String | Yes | -- | Appointment time as string (from CSV) |
1562
+ | `bentry_doctor_name` | String | Yes | `""` | Doctor name (if available in CSV) |
1563
+ | `bentry_department` | String | Yes | `""` | Department (if available) |
1564
+ | `bentry_notes` | String | Yes | `""` | Additional notes |
1565
+ | `bentry_raw_data` | Json | Yes | `"{}"` | Complete original CSV row as JSON |
1566
+ | `bentry_call_status` | EntryCallStatus | Yes | `pending` | Call outcome status |
1567
+ | `bentry_attempts` | Int | Yes | `0` | Number of call attempts |
1568
+ | `bentry_conversation_id` | String | No | `null` | ElevenLabs conversation ID |
1569
+ | `bentry_validation_result` | ValidationResult | No | `null` | Outcome: `confirmed`, `cancelled`, `reschedule`, `unresolved` |
1570
+ | `bentry_reschedule_request` | String | No | `null` | Patient's preferred new time (if rescheduling) |
1571
+ | `bentry_patient_message` | String | No | `null` | Notable patient message during validation call |
1572
+ | `bentry_call_duration_secs` | Int | Yes | `0` | Call duration |
1573
+ | `bentry_called_at` | DateTime | No | `null` | When the call was made |
1574
+ | `bentry_result_at` | DateTime | No | `null` | When the validation result was recorded |
1575
+ | `bentry_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1576
+ | `bentry_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1577
+
1578
+ **Indexes:**
1579
+
1580
+ | Index | Columns | Purpose |
1581
+ |-------|---------|---------|
1582
+ | Primary | `bentry_id` | PK lookup |
1583
+ | `idx_bentries_batch_status` | `(bentry_batch_id, bentry_call_status)` | Dispatch pending entries, aggregate by status |
1584
+ | `idx_bentries_conversation` | `bentry_conversation_id` | Resolve ElevenLabs post-call webhook and tool callback |
1585
+
1586
+ **Relations:**
1587
+
1588
+ | Direction | Target | FK Column | Description |
1589
+ |-----------|--------|-----------|-------------|
1590
+ | belongs_to | appointment_validation_batches | `bentry_batch_id` | Parent batch (cascade delete) |
1591
+ | belongs_to | appointments | `bentry_appointment_id` | Linked appointment in our system (optional) |
1592
+
1593
+ **Design decisions:**
1594
+ - `bentry_appointment_id` enables closing the loop: when a patient confirms/cancels during the validation call, the system can auto-update the linked appointment's status (`confirmed`, `cancelled`). Nullable because CSV imports may contain appointments from external systems not in our database.
1595
+ - `bentry_appointment_date` and `bentry_appointment_time` are stored as strings, not DateTimes. The data comes from CSV and may have inconsistent formats. The AI agent receives the raw strings and interprets them.
1596
+ - `bentry_raw_data` preserves the complete original CSV row as JSONB. This ensures no data loss during column mapping and enables re-export.
1597
+ - Cascade delete from batch: deleting a batch removes all its entries.
1598
+
1599
+ ---
1600
+
1601
+ ### 24. billing_events
1602
+
1603
+ **Purpose:** Individual billing events (charges, subscriptions, top-ups, refunds) for usage tracking. Each voice call generates a `call_charge` event based on duration.
1604
+
1605
+ | Column | Type | Required | Default | Description |
1606
+ |--------|------|----------|---------|-------------|
1607
+ | `billing_id` | UUID | Yes | `uuid()` | Primary key |
1608
+ | `billing_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1609
+ | `billing_clinic_id` | UUID | No | `null` | FK to `clinics`. Which branch generated this charge. Null for tenant-level events (subscription, top_up) |
1610
+ | `billing_type` | BillingEventType | Yes | -- | `call_charge`, `subscription`, `top_up`, or `refund` |
1611
+ | `billing_amount` | Decimal | Yes | -- | Amount in TRY |
1612
+ | `billing_currency` | String | Yes | `"TRY"` | Currency code |
1613
+ | `billing_call_id` | UUID | No | `null` | FK to `calls`. Non-null for `call_charge` events |
1614
+ | `billing_description` | String | Yes | `""` | Human-readable description |
1615
+ | `billing_created_at` | DateTime | Yes | `now()` | Event timestamp |
1616
+
1617
+ **Indexes:**
1618
+
1619
+ | Index | Columns | Purpose |
1620
+ |-------|---------|---------|
1621
+ | Primary | `billing_id` | PK lookup |
1622
+ | `idx_billing_tenant_date` | `(billing_tenant_id, billing_created_at DESC)` | Billing history list |
1623
+
1624
+ **Relations:**
1625
+
1626
+ | Direction | Target | FK Column | Description |
1627
+ |-----------|--------|-----------|-------------|
1628
+ | belongs_to | tenants | `billing_tenant_id` | Parent tenant (cascade delete) |
1629
+ | belongs_to | clinics | `billing_clinic_id` | Source clinic (optional) |
1630
+ | belongs_to | calls | `billing_call_id` | Associated call (for call charges) |
1631
+
1632
+ **Design decisions:**
1633
+ - No `billing_updated_at`. Billing events are immutable once created.
1634
+ - `billing_clinic_id` enables per-clinic cost breakdown (`GROUP BY billing_clinic_id`). Null for tenant-level events like subscription payments and top-ups. Populated for call charges (copied from the call's clinic).
1635
+ - `Decimal` for `billing_amount` to avoid floating-point rounding.
1636
+ - Call charges are calculated at 1.5 TRY per minute.
1637
+
1638
+ ---
1639
+
1640
+ ### 25. payments
1641
+
1642
+ **Purpose:** Tenant subscription payments recorded by staff. Tracks who paid, how much, for which plan, and the billing period covered.
1643
+
1644
+ | Column | Type | Required | Default | Description |
1645
+ |--------|------|----------|---------|-------------|
1646
+ | `payment_id` | UUID | Yes | `uuid()` | Primary key |
1647
+ | `payment_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1648
+ | `payment_type` | PaymentType | Yes | `subscription` | `subscription` or `top_up` |
1649
+ | `payment_plan_slug` | String | Yes | -- | Plan slug at time of payment (denormalized) |
1650
+ | `payment_plan_name` | String | Yes | -- | Plan name at time of payment (denormalized) |
1651
+ | `payment_amount` | Decimal | Yes | -- | Amount paid in TRY |
1652
+ | `payment_extra_minutes` | Int | Yes | `0` | Extra call minutes purchased (for top-ups) |
1653
+ | `payment_period_start` | DateTime | Yes | -- | Start of the billing period this payment covers |
1654
+ | `payment_period_end` | DateTime | Yes | -- | End of the billing period |
1655
+ | `payment_method` | PaymentMethod | Yes | `cash` | How the payment was made |
1656
+ | `payment_notes` | String | Yes | `""` | Staff notes |
1657
+ | `payment_recorded_by_id` | UUID | Yes | -- | FK to `users`. Staff member who recorded the payment |
1658
+ | `payment_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1659
+ | `payment_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1660
+
1661
+ **Indexes:**
1662
+
1663
+ | Index | Columns | Purpose |
1664
+ |-------|---------|---------|
1665
+ | Primary | `payment_id` | PK lookup |
1666
+ | `idx_payments_tenant` | `payment_tenant_id` | List all payments for a tenant |
1667
+ | `idx_payments_tenant_period` | `(payment_tenant_id, payment_period_end DESC)` | Find active/latest billing period |
1668
+
1669
+ **Relations:**
1670
+
1671
+ | Direction | Target | FK Column | Description |
1672
+ |-----------|--------|-----------|-------------|
1673
+ | belongs_to | tenants | `payment_tenant_id` | Parent tenant (cascade delete) |
1674
+ | belongs_to | users (RecordedBy) | `payment_recorded_by_id` | Staff who recorded the payment |
1675
+ | has_many | billing_period_snapshots | -- | Usage snapshots for this period |
1676
+ | has_many | billing_alerts | -- | Alerts generated during this period |
1677
+
1678
+ **Design decisions:**
1679
+ - `payment_plan_slug` and `payment_plan_name` are denormalized from the `plans` table at time of payment. This ensures the payment record remains accurate even if the plan is later modified.
1680
+ - `payment_period_start` and `payment_period_end` define the billing window. Usage is tracked within this window.
1681
+
1682
+ ---
1683
+
1684
+ ### 26. billing_period_snapshots
1685
+
1686
+ **Purpose:** Snapshot of actual usage during a billing period. Created by a daily BullMQ job that checks for expired periods and captures final usage metrics.
1687
+
1688
+ | Column | Type | Required | Default | Description |
1689
+ |--------|------|----------|---------|-------------|
1690
+ | `snapshot_id` | UUID | Yes | `uuid()` | Primary key |
1691
+ | `snapshot_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1692
+ | `snapshot_payment_id` | UUID | Yes | -- | FK to `payments`. Which payment/period this snapshot covers |
1693
+ | `snapshot_plan_id` | UUID | No | `null` | FK to `plans`. Plan at time of snapshot (for limit comparison) |
1694
+ | `snapshot_period_start` | DateTime | Yes | -- | Start of the billing period |
1695
+ | `snapshot_period_end` | DateTime | Yes | -- | End of the billing period |
1696
+ | `snapshot_total_calls` | Int | Yes | `0` | Total voice calls |
1697
+ | `snapshot_inbound_calls` | Int | Yes | `0` | Inbound call count |
1698
+ | `snapshot_inbound_minutes` | Decimal | Yes | `0` | Inbound call minutes |
1699
+ | `snapshot_outbound_calls` | Int | Yes | `0` | Outbound call count |
1700
+ | `snapshot_outbound_minutes` | Decimal | Yes | `0` | Outbound call minutes |
1701
+ | `snapshot_total_charges` | Decimal | Yes | `0` | Total TRY charges |
1702
+ | `snapshot_wa_billable_chats` | Int | Yes | `0` | WhatsApp billable chat count |
1703
+ | `snapshot_wa_total_sessions` | Int | Yes | `0` | Total WhatsApp sessions |
1704
+ | `snapshot_wa_total_messages` | Int | Yes | `0` | Total WhatsApp messages |
1705
+ | `snapshot_wa_ai_messages` | Int | Yes | `0` | AI-sent WhatsApp messages |
1706
+ | `snapshot_wa_el_cost` | Decimal | Yes | `0` | ElevenLabs cost for WhatsApp |
1707
+ | `snapshot_wa_stt_count` | Int | Yes | `0` | Speech-to-text transcription count |
1708
+ | `snapshot_wa_stt_seconds` | Decimal | Yes | `0` | Speech-to-text total seconds |
1709
+ | `snapshot_created_at` | DateTime | Yes | `now()` | Snapshot timestamp |
1710
+
1711
+ **Indexes:**
1712
+
1713
+ | Index | Columns | Purpose |
1714
+ |-------|---------|---------|
1715
+ | Primary | `snapshot_id` | PK lookup |
1716
+ | `idx_snapshots_tenant_period` | `(snapshot_tenant_id, snapshot_period_start DESC)` | Historical usage lookup |
1717
+
1718
+ **Relations:**
1719
+
1720
+ | Direction | Target | FK Column | Description |
1721
+ |-----------|--------|-----------|-------------|
1722
+ | belongs_to | tenants | `snapshot_tenant_id` | Parent tenant (cascade delete) |
1723
+ | belongs_to | payments | `snapshot_payment_id` | Associated payment |
1724
+ | belongs_to | plans | `snapshot_plan_id` | Plan at time of snapshot |
1725
+
1726
+ **Design decisions:**
1727
+ - No `snapshot_updated_at`. Snapshots are immutable once created.
1728
+ - `Decimal` for all minute and cost fields to avoid floating-point rounding.
1729
+ - `snapshot_plan_id` captures the plan at the time of the snapshot so that usage vs. limits can be compared historically even if the plan changes later.
1730
+
1731
+ ---
1732
+
1733
+ ### 27. billing_alerts
1734
+
1735
+ **Purpose:** Tracks which billing alerts have been sent to prevent duplicates. Each combination of tenant + alert type + service + payment is unique.
1736
+
1737
+ | Column | Type | Required | Default | Description |
1738
+ |--------|------|----------|---------|-------------|
1739
+ | `alert_id` | UUID | Yes | `uuid()` | Primary key |
1740
+ | `alert_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1741
+ | `alert_type` | AlertType | Yes | -- | `usage_75`, `usage_90`, `usage_100`, or `period_expiring` |
1742
+ | `alert_service` | AlertService | Yes | -- | Which service/metric triggered the alert |
1743
+ | `alert_payment_id` | UUID | Yes | -- | FK to `payments`. Which billing period |
1744
+ | `alert_created_at` | DateTime | Yes | `now()` | When the alert was sent |
1745
+
1746
+ **Indexes:**
1747
+
1748
+ | Index | Columns | Purpose |
1749
+ |-------|---------|---------|
1750
+ | Primary | `alert_id` | PK lookup |
1751
+ | Unique | `(alert_tenant_id, alert_type, alert_service, alert_payment_id)` | Prevent sending the same alert twice |
1752
+
1753
+ **Relations:**
1754
+
1755
+ | Direction | Target | FK Column | Description |
1756
+ |-----------|--------|-----------|-------------|
1757
+ | belongs_to | tenants | `alert_tenant_id` | Parent tenant (cascade delete) |
1758
+ | belongs_to | payments | `alert_payment_id` | Billing period context |
1759
+
1760
+ **Design decisions:**
1761
+ - The four-column unique constraint prevents duplicate alerts. The daily billing alert job checks this constraint before sending.
1762
+ - No `alert_updated_at`. Alerts are immutable.
1763
+
1764
+ ---
1765
+
1766
+ ### 28. stt_usages
1767
+
1768
+ **Purpose:** Tracks speech-to-text usage for billing and analytics. One record per voice message transcription.
1769
+
1770
+ | Column | Type | Required | Default | Description |
1771
+ |--------|------|----------|---------|-------------|
1772
+ | `stt_id` | UUID | Yes | `uuid()` | Primary key |
1773
+ | `stt_tenant_id` | UUID | Yes | -- | FK to `tenants` |
1774
+ | `stt_clinic_id` | UUID | No | `null` | FK to `clinics`. Which branch generated this usage |
1775
+ | `stt_chat_id` | String | Yes | `""` | WhatsApp chat ID (informational, not an FK) |
1776
+ | `stt_message_id` | String | Yes | `""` | Message ID (informational) |
1777
+ | `stt_duration_seconds` | Decimal | Yes | -- | Audio duration in seconds |
1778
+ | `stt_character_count` | Int | Yes | -- | Characters in the transcribed text |
1779
+ | `stt_language` | String | Yes | `"tr"` | Transcription language |
1780
+ | `stt_created_at` | DateTime | Yes | `now()` | Transcription timestamp |
1781
+
1782
+ **Indexes:**
1783
+
1784
+ | Index | Columns | Purpose |
1785
+ |-------|---------|---------|
1786
+ | Primary | `stt_id` | PK lookup |
1787
+ | `idx_stt_tenant_date` | `(stt_tenant_id, stt_created_at DESC)` | Usage aggregation for billing |
1788
+
1789
+ **Relations:**
1790
+
1791
+ | Direction | Target | FK Column | Description |
1792
+ |-----------|--------|-----------|-------------|
1793
+ | belongs_to | tenants | `stt_tenant_id` | Parent tenant (cascade delete) |
1794
+
1795
+ **Design decisions:**
1796
+ - `stt_chat_id` and `stt_message_id` are String fields, not FKs. They are informational references for debugging. No join needed.
1797
+ - No `stt_updated_at`. Usage records are immutable.
1798
+
1799
+ ---
1800
+
1801
+ ### 29. logs
1802
+
1803
+ **Purpose:** Application logs stored in PostgreSQL for the admin dashboard. Only `info` level and above are persisted to the database (debug/trace stay in console/file). Written asynchronously in batches (max 100, flush every 5 seconds).
1804
+
1805
+ | Column | Type | Required | Default | Description |
1806
+ |--------|------|----------|---------|-------------|
1807
+ | `log_id` | UUID | Yes | `uuid()` | Primary key |
1808
+ | `log_level` | String | Yes | -- | Log level: `fatal`, `error`, `warn`, `info` |
1809
+ | `log_message` | String | Yes | -- | Human-readable log message |
1810
+ | `log_service` | String | Yes | `"api"` | Which service/module (e.g., `"whatsapp-agent"`, `"billing"`, `"calendar.tools"`) |
1811
+ | `log_meta` | Json | Yes | `"{}"` | Structured data payload |
1812
+ | `log_request_id` | String | No | `null` | Correlation ID (traces a request across services/queues) |
1813
+ | `log_tenant_id` | String | No | `null` | Tenant context (not an FK -- logs may outlive tenants) |
1814
+ | `log_clinic_id` | String | No | `null` | Clinic context |
1815
+ | `log_user_id` | String | No | `null` | User context |
1816
+ | `log_timestamp` | DateTime | Yes | `now()` | Log event timestamp |
1817
+
1818
+ **Indexes:**
1819
+
1820
+ | Index | Columns | Purpose |
1821
+ |-------|---------|---------|
1822
+ | Primary | `log_id` | PK lookup |
1823
+ | `idx_logs_level` | `log_level` | Filter by level |
1824
+ | `idx_logs_timestamp` | `log_timestamp` | Time-range queries |
1825
+ | `idx_logs_level_timestamp` | `(log_level, log_timestamp DESC)` | Filter by level + time range (admin dashboard) |
1826
+ | `idx_logs_tenant_timestamp` | `(log_tenant_id, log_timestamp DESC)` | Tenant-scoped log viewing |
1827
+ | `idx_logs_request` | `log_request_id` | Trace all logs for a single request |
1828
+ | `idx_logs_service_timestamp` | `(log_service, log_timestamp DESC)` | Filter by service |
1829
+
1830
+ **Relations:** None. Log columns like `log_tenant_id` are strings, not FKs, because logs may need to survive tenant deletion and should never cause cascading failures.
1831
+
1832
+ **Design decisions:**
1833
+ - Six indexes -- logs are the most-read table in the admin panel and need fast filtering across multiple dimensions.
1834
+ - No foreign keys. Logs are a standalone observability layer. Tenant/user/clinic IDs are strings for informational purposes.
1835
+ - Cleanup: BullMQ job runs daily at 3am, deletes logs older than configurable retention (default 30 days).
1836
+ - Written via async batched writer to avoid impacting request performance.
1837
+
1838
+ ---
1839
+
1840
+ ### 30. audit_logs
1841
+
1842
+ **Purpose:** Immutable audit trail tracking who did what, when, and what changed. Separate from application logs. Never auto-deleted. Used for compliance (KVKK), accountability, and forensics.
1843
+
1844
+ | Column | Type | Required | Default | Description |
1845
+ |--------|------|----------|---------|-------------|
1846
+ | `audit_id` | UUID | Yes | `uuid()` | Primary key |
1847
+ | `audit_tenant_id` | String | No | `null` | Tenant context. Null for platform actions (e.g., sales creating tenant) |
1848
+ | `audit_clinic_id` | String | No | `null` | Clinic context. Null for tenant-level actions |
1849
+ | `audit_user_id` | String | No | `null` | Acting user. Null for system actions (cron, AI) |
1850
+ | `audit_user_email` | String | No | `null` | Denormalized email -- survives user deletion |
1851
+ | `audit_user_role` | String | No | `null` | User's role at the time of the action |
1852
+ | `audit_action` | String | Yes | -- | Action identifier (e.g., `"clinic.settings.updated"`, `"appointment.created"`) |
1853
+ | `audit_entity` | String | Yes | -- | Entity type (e.g., `"Tenant"`, `"Appointment"`, `"User"`) |
1854
+ | `audit_entity_id` | String | Yes | -- | ID of the affected record |
1855
+ | `audit_changes` | Json | No | `null` | Before/after diff: `{field: {old: x, new: y}}`. Null for non-update actions |
1856
+ | `audit_metadata` | Json | No | `null` | Extra context: `{ip, userAgent, requestId, reason}` |
1857
+ | `audit_created_at` | DateTime | Yes | `now()` | Action timestamp |
1858
+
1859
+ **Indexes:**
1860
+
1861
+ | Index | Columns | Purpose |
1862
+ |-------|---------|---------|
1863
+ | Primary | `audit_id` | PK lookup |
1864
+ | `idx_audit_tenant_date` | `(audit_tenant_id, audit_created_at DESC)` | Tenant audit trail |
1865
+ | `idx_audit_clinic_date` | `(audit_clinic_id, audit_created_at DESC)` | Clinic audit trail |
1866
+ | `idx_audit_entity` | `(audit_entity, audit_entity_id)` | History of a specific record |
1867
+ | `idx_audit_user_date` | `(audit_user_id, audit_created_at DESC)` | All actions by a specific user |
1868
+ | `idx_audit_action_date` | `(audit_action, audit_created_at DESC)` | Filter by action type |
1869
+
1870
+ **Relations:** None. Like logs, audit entries use string IDs rather than FKs to ensure they survive entity deletion.
1871
+
1872
+ **Design decisions:**
1873
+ - No `audit_updated_at`. Audit logs are append-only and immutable.
1874
+ - `audit_user_email` and `audit_user_role` are denormalized so the audit trail remains meaningful even after users are deleted.
1875
+ - `audit_changes` captures the diff as `{field: {old, new}}` for update actions. Null for creates, deletes, and non-mutation actions (like login).
1876
+ - Written synchronously (not batched) because audit events must not be lost.
1877
+ - Retention: never auto-deleted. For large tenants, old audit logs (>2 years) can be manually archived to MinIO.
1878
+
1879
+ ---
1880
+
1881
+ ### 31. tool_execution_logs
1882
+
1883
+ **Purpose:** Tracks every AI tool execution (e.g., `check_availability`, `reserve_slot`, `forward_to_operator`) for observability and analytics. Written fire-and-forget by the ToolRegistryService.
1884
+
1885
+ | Column | Type | Required | Default | Description |
1886
+ |--------|------|----------|---------|-------------|
1887
+ | `toollog_id` | UUID | Yes | `uuid()` | Primary key |
1888
+ | `toollog_tenant_id` | String | Yes | -- | Tenant context |
1889
+ | `toollog_clinic_id` | String | No | `null` | Clinic context |
1890
+ | `toollog_tool_name` | String | Yes | -- | Tool name (e.g., `"check_availability"`) |
1891
+ | `toollog_channel` | String | Yes | -- | Channel: `"whatsapp"`, `"voice_call"`, `"instagram"`, `"test_chat"` |
1892
+ | `toollog_patient_phone` | String | Yes | `""` | Patient phone number |
1893
+ | `toollog_conversation_id` | String | No | `null` | ElevenLabs conversation ID |
1894
+ | `toollog_parameters` | Json | Yes | `"{}"` | Parameters passed to the tool |
1895
+ | `toollog_result` | Json | Yes | `"{}"` | Tool execution result |
1896
+ | `toollog_is_error` | Boolean | Yes | `false` | Whether the execution resulted in an error |
1897
+ | `toollog_duration_ms` | Int | Yes | `0` | Execution time in milliseconds |
1898
+ | `toollog_created_at` | DateTime | Yes | `now()` | Execution timestamp |
1899
+
1900
+ **Indexes:**
1901
+
1902
+ | Index | Columns | Purpose |
1903
+ |-------|---------|---------|
1904
+ | Primary | `toollog_id` | PK lookup |
1905
+ | `idx_toollog_tenant_date` | `(toollog_tenant_id, toollog_created_at DESC)` | Tenant-scoped tool log list |
1906
+ | `idx_toollog_tool_date` | `(toollog_tool_name, toollog_created_at DESC)` | Filter by tool name |
1907
+ | `idx_toollog_conversation` | `toollog_conversation_id` | Find all tool calls in a conversation |
1908
+
1909
+ **Relations:** None. Uses string IDs for tenant reference to avoid cascading issues.
1910
+
1911
+ **Design decisions:**
1912
+ - No foreign keys. Like logs and audit logs, this is an observability table.
1913
+ - Written fire-and-forget with `.catch(() => {})` to never impact tool execution performance.
1914
+ - Cleanup: BullMQ job deletes logs older than 30 days (or per-tenant retention policy).
1915
+
1916
+ ---
1917
+
1918
+ ### 32. migrations
1919
+
1920
+ **Purpose:** Tracks custom data migration runs (not Prisma schema migrations -- those are managed by `prisma migrate`). Used for one-time data transformations, backfills, and cross-system imports.
1921
+
1922
+ | Column | Type | Required | Default | Description |
1923
+ |--------|------|----------|---------|-------------|
1924
+ | `migration_id` | UUID | Yes | `uuid()` | Primary key |
1925
+ | `migration_name` | String | Yes (unique) | -- | Migration identifier (e.g., `"backfill-clinic-slugs"`) |
1926
+ | `migration_ran_at` | DateTime | Yes | -- | When the migration ran |
1927
+ | `migration_duration_ms` | Int | Yes | -- | How long it took in milliseconds |
1928
+ | `migration_status` | MigrationStatus | Yes | -- | `success` or `failed` |
1929
+ | `migration_error` | String | No | `null` | Error message if failed |
1930
+ | `migration_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1931
+ | `migration_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1932
+
1933
+ **Indexes:**
1934
+
1935
+ | Index | Columns | Purpose |
1936
+ |-------|---------|---------|
1937
+ | Primary | `migration_id` | PK lookup |
1938
+ | Unique | `migration_name` | Prevent running the same migration twice |
1939
+
1940
+ **Relations:** None.
1941
+
1942
+ **Design decisions:**
1943
+ - The unique constraint on `migration_name` ensures idempotency. A migration script checks this table before running.
1944
+ - This table is for application-level data migrations only. Schema migrations are tracked by Prisma's `_prisma_migrations` table.
1945
+
1946
+ ---
1947
+
1948
+ ### 33. transcriptions
1949
+
1950
+ > **TODO:** Revisit overlap with `call_details` table (which also stores transcript + summary). These tables may serve as a processing pipeline/staging area, or they may be consolidated into call_details.
1951
+
1952
+ **Purpose:** Stores post-call transcriptions received from ElevenLabs webhooks. Each transcription has a processing pipeline: received -> summarized -> notified.
1953
+
1954
+ | Column | Type | Required | Default | Description |
1955
+ |--------|------|----------|---------|-------------|
1956
+ | `tx_id` | UUID | Yes | `uuid()` | Primary key |
1957
+ | `tx_conversation_id` | String | Yes (unique) | -- | ElevenLabs conversation ID |
1958
+ | `tx_agent_id` | String | Yes | -- | ElevenLabs agent ID |
1959
+ | `tx_agent_name` | String | No | `null` | Agent name (denormalized for display) |
1960
+ | `tx_user_id` | String | No | `null` | User/caller identifier from ElevenLabs |
1961
+ | `tx_event_type` | String | Yes | `"post_call_transcription"` | Event type |
1962
+ | `tx_event_timestamp` | Int | No | `null` | Unix timestamp from ElevenLabs |
1963
+ | `tx_status` | String | Yes | `"done"` | ElevenLabs-reported status |
1964
+ | `tx_transcript` | Json | Yes | `"[]"` | Array of transcript entries `[{role, message, timestamp}]` |
1965
+ | `tx_metadata` | Json | No | `null` | ElevenLabs metadata payload |
1966
+ | `tx_analysis` | Json | No | `null` | ElevenLabs analysis payload |
1967
+ | `tx_processing_status` | TranscriptionProcessingStatus | Yes | `pending` | Pipeline stage |
1968
+ | `tx_processing_error` | String | No | `null` | Error message if processing failed |
1969
+ | `tx_raw_payload` | Json | No | `null` | Complete raw webhook payload (for debugging) |
1970
+ | `tx_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
1971
+ | `tx_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
1972
+
1973
+ **Indexes:**
1974
+
1975
+ | Index | Columns | Purpose |
1976
+ |-------|---------|---------|
1977
+ | Primary | `tx_id` | PK lookup |
1978
+ | Unique | `tx_conversation_id` | One transcription per conversation |
1979
+ | `idx_tx_agent` | `tx_agent_id` | Filter by agent |
1980
+ | `idx_tx_user` | `tx_user_id` | Filter by caller |
1981
+ | `idx_tx_processing` | `tx_processing_status` | Find unprocessed transcriptions (BullMQ) |
1982
+ | `idx_tx_date` | `tx_created_at DESC` | Chronological listing |
1983
+
1984
+ **Relations:**
1985
+
1986
+ | Direction | Target | FK Column | Description |
1987
+ |-----------|--------|-----------|-------------|
1988
+ | has_one | summaries | -- | AI-generated summary of this transcription |
1989
+
1990
+ **Design decisions:**
1991
+ - `tx_transcript` is JSONB because the transcript is an array of objects with variable structure from ElevenLabs.
1992
+ - `tx_raw_payload` stores the complete webhook payload for debugging. Can be large but is rarely accessed.
1993
+ - The processing pipeline is: `pending` -> `summarizing` -> `summarized` -> `notified` (or `failed` at any step).
1994
+
1995
+ ---
1996
+
1997
+ ### 34. summaries
1998
+
1999
+ > **TODO:** Same as transcriptions — revisit overlap with `call_details.detail_llm_summary`.
2000
+
2001
+ **Purpose:** AI-generated summaries of call transcriptions. Created asynchronously by the summary generation pipeline.
2002
+
2003
+ | Column | Type | Required | Default | Description |
2004
+ |--------|------|----------|---------|-------------|
2005
+ | `summary_id` | UUID | Yes | `uuid()` | Primary key |
2006
+ | `summary_transcription_id` | UUID | Yes (unique) | -- | FK to `transcriptions`. One-to-one |
2007
+ | `summary_conversation_id` | String | Yes | -- | ElevenLabs conversation ID (denormalized for direct lookup) |
2008
+ | `summary_text` | String | Yes | -- | Full summary text |
2009
+ | `summary_title` | String | No | `null` | Short title/headline |
2010
+ | `summary_key_points` | String[] | Yes | `[]` | Bullet-point key takeaways |
2011
+ | `summary_detected_intent` | String | No | `null` | AI-detected caller intent |
2012
+ | `summary_sentiment` | Sentiment | No | `null` | AI-detected sentiment |
2013
+ | `summary_ai_provider` | String | Yes | -- | Which AI provider generated this (`"openai"` or `"anthropic"`) |
2014
+ | `summary_ai_model` | String | No | `null` | Specific model used |
2015
+ | `summary_token_usage` | Json | No | `null` | Token usage: `{promptTokens, completionTokens, totalTokens}` |
2016
+ | `summary_notification_status` | NotificationDeliveryStatus | Yes | `pending` | Status of summary notification to staff |
2017
+ | `summary_notified_recipients` | Json | Yes | `"[]"` | Array of recipients who were notified |
2018
+ | `summary_language` | String | Yes | `"tr"` | Language of the summary |
2019
+ | `summary_created_at` | DateTime | Yes | `now()` | Summary generation timestamp |
2020
+ | `summary_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2021
+
2022
+ **Indexes:**
2023
+
2024
+ | Index | Columns | Purpose |
2025
+ |-------|---------|---------|
2026
+ | Primary | `summary_id` | PK lookup |
2027
+ | Unique | `summary_transcription_id` | One-to-one with transcriptions |
2028
+ | `idx_summary_conversation` | `summary_conversation_id` | Direct lookup by conversation |
2029
+
2030
+ **Relations:**
2031
+
2032
+ | Direction | Target | FK Column | Description |
2033
+ |-----------|--------|-----------|-------------|
2034
+ | belongs_to | transcriptions | `summary_transcription_id` | Source transcription |
2035
+
2036
+ **Design decisions:**
2037
+ - One-to-one with transcriptions via unique constraint.
2038
+ - `summary_ai_provider` and `summary_ai_model` track which AI generated the summary for cost tracking and quality comparison.
2039
+ - `summary_token_usage` is JSONB because the structure varies slightly between providers.
2040
+ - `summary_key_points` is a `String[]` array for easy rendering as bullet points.
2041
+
2042
+ ---
2043
+
2044
+ ### 35. ~~whatsapp_recipients~~ — REMOVED
2045
+
2046
+ > **This table has been removed.** Legacy table with no tenant scoping. Notification recipients are now handled by `clinic_notification_phones` and `clinic_sms_phones` on the clinics table. WhatsApp group notifications can be reimplemented properly when needed.
2047
+
2048
+ ---
2049
+
2050
+ ## Clinic Management Tables
2051
+
2052
+ > The following tables were added as part of the clinic management pivot.
2053
+ > Patient is the central entity. Treatment catalog, treatment plans, doctor profiles,
2054
+ > and before/after photos support the core clinic workflow.
2055
+
2056
+ ---
2057
+
2058
+ ### 36. patients
2059
+
2060
+ **Purpose:** Central entity in the system. Every person who contacts or visits the clinic gets a Patient record. Replaces `whatsapp_contact_profiles` (#19). Auto-created on first AI contact (WhatsApp, voice call, Instagram). Enriched manually by staff from the dashboard.
2061
+
2062
+ | Column | Type | Required | Default | Description |
2063
+ |--------|------|----------|---------|-------------|
2064
+ | `patient_id` | UUID | Yes | `uuid()` | Primary key |
2065
+ | `patient_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2066
+ | `patient_clinic_id` | UUID | No | `null` | FK to `clinics`. Primary clinic (nullable for unassigned) |
2067
+ | `patient_phone` | String | Yes | -- | Primary phone number (E.164) |
2068
+ | `patient_email` | String | No | `null` | Email address |
2069
+ | `patient_first_name` | String | Yes | `""` | First name |
2070
+ | `patient_last_name` | String | Yes | `""` | Last name |
2071
+ | `patient_display_name` | String | Yes | `""` | Auto-built from first+last, or from WhatsApp profile name |
2072
+ | `patient_date_of_birth` | DateTime | No | `null` | Date of birth |
2073
+ | `patient_gender` | String | No | `null` | `male`, `female`, `other` |
2074
+ | `patient_id_number` | String | No | `null` | **Encrypted.** TC Kimlik No. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2075
+ | `patient_blood_type` | String | No | `null` | Blood type (e.g., `A+`, `O-`) |
2076
+ | `patient_preferred_language` | String | Yes | `"tr"` | Preferred communication language |
2077
+ | `patient_source` | String | No | `null` | How the patient found the clinic: `whatsapp`, `voice_call`, `instagram`, `dashboard`, `walk_in`, `referral`, `campaign` |
2078
+ | `patient_source_detail` | String | No | `null` | Campaign name, referrer name, ad ID |
2079
+ | `patient_tags` | String[] | Yes | `[]` | Free-form tags for segmentation |
2080
+ | `patient_ai_summary` | String | Yes | `""` | **Encrypted.** AI-generated summary from conversations. AES-256-GCM via `PATIENT_SUMMARY_ENCRYPTION_KEY` |
2081
+ | `patient_is_active` | Boolean | Yes | `true` | Soft-disable. Inactive patients are hidden from lists |
2082
+ | `patient_first_contact_at` | DateTime | Yes | `now()` | First interaction timestamp |
2083
+ | `patient_last_contact_at` | DateTime | Yes | `now()` | Most recent interaction timestamp |
2084
+ | `patient_total_sessions` | Int | Yes | `0` | Denormalized WhatsApp session count |
2085
+ | `patient_total_appointments` | Int | Yes | `0` | Denormalized appointment count |
2086
+ | `patient_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2087
+ | `patient_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2088
+
2089
+ **Indexes:**
2090
+
2091
+ | Index | Columns | Purpose |
2092
+ |-------|---------|---------|
2093
+ | Primary | `patient_id` | PK lookup |
2094
+ | Unique | `(patient_tenant_id, patient_phone)` | One patient per phone per tenant |
2095
+ | `idx_patients_tenant_lastcontact` | `(patient_tenant_id, patient_last_contact_at DESC)` | Patient list sorted by recent activity |
2096
+ | `idx_patients_clinic` | `patient_clinic_id` | Patients by clinic |
2097
+ | `idx_patients_tenant_name` | `(patient_tenant_id, patient_last_name, patient_first_name)` | Name search |
2098
+
2099
+ **Relations:**
2100
+
2101
+ | Direction | Target | FK Column | Description |
2102
+ |-----------|--------|-----------|-------------|
2103
+ | belongs_to | tenants | `patient_tenant_id` | Parent tenant (cascade delete) |
2104
+ | belongs_to | clinics | `patient_clinic_id` | Primary clinic |
2105
+ | has_one | patient_medical_histories | -- | Medical history / anamnesis |
2106
+ | has_many | treatment_plans | -- | Treatment plans for this patient |
2107
+ | has_many | appointments | -- | All appointments |
2108
+ | has_many | patient_photo_sets | -- | Before/after photos |
2109
+ | has_many | patient_documents | -- | Documents (consent, x-ray, etc.) |
2110
+ | has_many | patient_notes | -- | Staff notes |
2111
+ | has_many | whatsapp_chats | -- | WhatsApp/Instagram conversations |
2112
+ | has_many | operator_requests | -- | Via session → chat → patient |
2113
+ | has_many | leads | -- | Leads linked to this patient |
2114
+
2115
+ **Design decisions:**
2116
+ - Replaces `whatsapp_contact_profiles`. All ContactProfile fields migrated here (see doc 19-PATIENTS.md Section 11).
2117
+ - `patient_phone` is the natural matching key. On first WhatsApp/voice contact, the system creates a Patient from the phone number.
2118
+ - `patient_display_name` is auto-built as `"{first_name} {last_name}"` on update, or initialized from the WhatsApp profile name on auto-creation.
2119
+ - `patient_source` is a free-form string (not enum) because clinics will add custom sources.
2120
+ - Denormalized counters (`total_sessions`, `total_appointments`) avoid expensive COUNT queries on patient list views.
2121
+ - **Two encryption keys for patient data:** `PATIENT_PII_ENCRYPTION_KEY` for national ID (TC Kimlik), `PATIENT_SUMMARY_ENCRYPTION_KEY` for AI summary. Separate from `MESSAGE_ENCRYPTION_KEY` to limit blast radius.
2122
+
2123
+ ---
2124
+
2125
+ ### 37. patient_medical_histories
2126
+
2127
+ **Purpose:** Static anamnesis form for a patient. Background health info filled once during first consultation, updated occasionally. One-to-one with `patients`. Per-visit clinical records are in `examination_records` (#46).
2128
+
2129
+ | Column | Type | Required | Default | Description |
2130
+ |--------|------|----------|---------|-------------|
2131
+ | `history_id` | UUID | Yes | `uuid()` | Primary key |
2132
+ | `history_patient_id` | UUID | Yes (unique) | -- | FK to `patients`. One-to-one |
2133
+ | `history_allergies` | String | Yes | `""` | **Encrypted.** Known allergies as JSON string. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2134
+ | `history_chronic_conditions` | String | Yes | `""` | **Encrypted.** Chronic conditions as JSON string. Same key |
2135
+ | `history_current_medications` | String | Yes | `""` | **Encrypted.** Current medications as JSON string. Same key |
2136
+ | `history_past_surgeries` | String | Yes | `""` | **Encrypted.** Description of past surgeries. Same key |
2137
+ | `history_pregnancy_status` | String | No | `null` | `not_applicable`, `not_pregnant`, `pregnant`, `unknown`. Not encrypted (non-identifying enum) |
2138
+ | `history_smoking` | Boolean | No | `null` | Whether the patient smokes. Not encrypted |
2139
+ | `history_alcohol` | Boolean | No | `null` | Whether the patient drinks alcohol. Not encrypted |
2140
+ | `history_notes` | String | Yes | `""` | **Encrypted.** Free-text medical notes. Same key |
2141
+ | `history_anamnesis_completed` | Boolean | Yes | `false` | Whether the anamnesis form has been filled |
2142
+ | `history_anamnesis_date` | DateTime | No | `null` | When the anamnesis was completed |
2143
+ | `history_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2144
+ | `history_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2145
+
2146
+ **Indexes:**
2147
+
2148
+ | Index | Columns | Purpose |
2149
+ |-------|---------|---------|
2150
+ | Primary | `history_id` | PK lookup |
2151
+ | Unique | `history_patient_id` | Enforce one-to-one with patients |
2152
+
2153
+ **Relations:**
2154
+
2155
+ | Direction | Target | FK Column | Description |
2156
+ |-----------|--------|-----------|-------------|
2157
+ | belongs_to | patients | `history_patient_id` | Parent patient (cascade delete) |
2158
+
2159
+ **Design decisions:**
2160
+ - Separate table (not JSONB on patient) because medical history is a distinct concern with its own access patterns, updated independently, and may grow with regulation-required fields.
2161
+ - **All text medical content is encrypted** with `PATIENT_PII_ENCRYPTION_KEY`. Allergies, conditions, medications, surgeries, and notes contain sensitive health data.
2162
+ - Array fields (`allergies`, `chronic_conditions`, `medications`) changed from `String[]` to encrypted `String`. The application stores them as JSON-serialized arrays, encrypted before write, decrypted and parsed on read. Encrypted content cannot use PostgreSQL array operations.
2163
+ - Boolean fields (`smoking`, `alcohol`) and enum fields (`pregnancy_status`) are NOT encrypted — they are non-identifying and needed for quick UI display.
2164
+ - `history_anamnesis_completed` flag lets the UI show "anamnesis pending" badges.
2165
+ - This is the **static background**. Per-visit clinical findings go in `examination_records` (#46).
2166
+
2167
+ ---
2168
+
2169
+ ### 38. patient_documents
2170
+
2171
+ **Purpose:** Files attached to a patient record. Stored in MinIO, referenced by object key. Consent forms, X-rays, lab results, prescriptions, referral letters.
2172
+
2173
+ | Column | Type | Required | Default | Description |
2174
+ |--------|------|----------|---------|-------------|
2175
+ | `doc_id` | UUID | Yes | `uuid()` | Primary key |
2176
+ | `doc_patient_id` | UUID | Yes | -- | FK to `patients` |
2177
+ | `doc_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2178
+ | `doc_clinic_id` | UUID | Yes | -- | FK to `clinics`. Which branch uploaded this |
2179
+ | `doc_appointment_id` | UUID | No | `null` | FK to `appointments`. Which visit this document belongs to |
2180
+ | `doc_type` | String | Yes | `"other"` | Document type: `consent_form`, `lab_result`, `x_ray`, `prescription`, `referral`, `other` |
2181
+ | `doc_name` | String | Yes | -- | Original file name |
2182
+ | `doc_media_key` | String | Yes | -- | MinIO object key: `{tenantId}/patients/{patientId}/documents/{docId}.{ext}` |
2183
+ | `doc_content_type` | String | Yes | -- | MIME type (e.g., `application/pdf`, `image/jpeg`) |
2184
+ | `doc_size_bytes` | Int | Yes | `0` | File size in bytes |
2185
+ | `doc_uploaded_by` | UUID | No | `null` | FK to `users`. Who uploaded the document |
2186
+ | `doc_notes` | String | Yes | `""` | **Encrypted.** Notes about the document. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2187
+ | `doc_created_at` | DateTime | Yes | `now()` | Upload timestamp |
2188
+
2189
+ **Indexes:**
2190
+
2191
+ | Index | Columns | Purpose |
2192
+ |-------|---------|---------|
2193
+ | Primary | `doc_id` | PK lookup |
2194
+ | `idx_docs_patient_date` | `(doc_patient_id, doc_created_at DESC)` | Patient document list (most recent first) |
2195
+ | `idx_docs_tenant_type` | `(doc_tenant_id, doc_type)` | Filter by type across tenant |
2196
+
2197
+ **Relations:**
2198
+
2199
+ | Direction | Target | FK Column | Description |
2200
+ |-----------|--------|-----------|-------------|
2201
+ | belongs_to | patients | `doc_patient_id` | Parent patient (cascade delete) |
2202
+ | belongs_to | tenants | `doc_tenant_id` | Parent tenant |
2203
+ | belongs_to | clinics | `doc_clinic_id` | Which clinic uploaded this |
2204
+ | belongs_to | appointments | `doc_appointment_id` | Which visit (optional) |
2205
+ | belongs_to | users | `doc_uploaded_by` | Uploader |
2206
+
2207
+ **Design decisions:**
2208
+ - No `doc_updated_at`. Documents are immutable once uploaded. To replace a document, delete the old one and upload a new one.
2209
+ - `doc_appointment_id` links a document to a specific visit. Enables "show all documents from this appointment" on the examination record. Nullable — not all documents are visit-specific (e.g., general consent form).
2210
+ - `doc_notes` encrypted with `PATIENT_PII_ENCRYPTION_KEY` — may contain clinical observations about the document.
2211
+ - `doc_type` is a string, not a Prisma enum, because clinics may need custom document types.
2212
+ - File content is in MinIO. Database stores only the key. Served via signed URL (1hr TTL).
2213
+ - On KVKK deletion: both the DB record and MinIO object are removed.
2214
+
2215
+ ---
2216
+
2217
+ ### 39. patient_notes
2218
+
2219
+ **Purpose:** Free-text notes added by staff to a patient record. Simple and lightweight — not a chat, just clinical or administrative observations.
2220
+
2221
+ | Column | Type | Required | Default | Description |
2222
+ |--------|------|----------|---------|-------------|
2223
+ | `note_id` | UUID | Yes | `uuid()` | Primary key |
2224
+ | `note_patient_id` | UUID | Yes | -- | FK to `patients` |
2225
+ | `note_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2226
+ | `note_clinic_id` | UUID | Yes | -- | FK to `clinics`. Which branch |
2227
+ | `note_author_id` | UUID | Yes | -- | FK to `users`. Who wrote the note |
2228
+ | `note_text` | String | Yes | -- | **Encrypted.** Note content. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2229
+ | `note_is_pinned` | Boolean | Yes | `false` | Pinned notes show at top of patient detail |
2230
+ | `note_created_at` | DateTime | Yes | `now()` | Creation timestamp |
2231
+ | `note_updated_at` | DateTime | Yes | auto | Automatically updated on edit |
2232
+
2233
+ **Indexes:**
2234
+
2235
+ | Index | Columns | Purpose |
2236
+ |-------|---------|---------|
2237
+ | Primary | `note_id` | PK lookup |
2238
+ | `idx_notes_patient_date` | `(note_patient_id, note_created_at DESC)` | Patient notes list (most recent first) |
2239
+
2240
+ **Relations:**
2241
+
2242
+ | Direction | Target | FK Column | Description |
2243
+ |-----------|--------|-----------|-------------|
2244
+ | belongs_to | patients | `note_patient_id` | Parent patient (cascade delete) |
2245
+ | belongs_to | tenants | `note_tenant_id` | Parent tenant |
2246
+ | belongs_to | clinics | `note_clinic_id` | Which clinic |
2247
+ | belongs_to | users | `note_author_id` | Note author |
2248
+
2249
+ **Design decisions:**
2250
+ - `note_text` encrypted with `PATIENT_PII_ENCRYPTION_KEY` — notes may contain clinical or personal observations.
2251
+ - Author is always required. Anonymous notes are not allowed.
2252
+ - Pinned notes are a simple boolean — no ordering among pinned notes (most recent first within pinned).
2253
+ - Notes are editable (has `updated_at`), but audit trail tracks changes via the `@Auditable()` interceptor.
2254
+ - No appointment link — clinical per-visit documentation goes in `examination_records` (#46). Notes are for general observations.
2255
+
2256
+ ---
2257
+
2258
+ ### 40. treatments
2259
+
2260
+ **Purpose:** Service catalog for a clinic. Each treatment defines what the clinic offers: name, category, duration, price. Used in treatment plans and appointment booking.
2261
+
2262
+ | Column | Type | Required | Default | Description |
2263
+ |--------|------|----------|---------|-------------|
2264
+ | `treatment_id` | UUID | Yes | `uuid()` | Primary key |
2265
+ | `treatment_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2266
+ | `treatment_clinic_id` | UUID | No | `null` | FK to `clinics`. Null = available at all clinics in the tenant |
2267
+ | `treatment_name` | String | Yes | -- | Treatment name (e.g., "Root Canal", "Botox") |
2268
+ | `treatment_category` | String | Yes | `""` | Category (e.g., "Implant", "Ortodonti", "Estetik") |
2269
+ | `treatment_description` | String | Yes | `""` | Description of the treatment |
2270
+ | `treatment_duration_minutes` | Int | Yes | `30` | Default appointment duration for this treatment |
2271
+ | `treatment_price` | Decimal | No | `null` | Base price. Null = price varies / not listed |
2272
+ | `treatment_currency` | String | Yes | `"TRY"` | Currency code |
2273
+ | `treatment_is_active` | Boolean | Yes | `true` | Soft-disable. Inactive treatments hidden from catalog |
2274
+ | `treatment_sort_order` | Int | Yes | `0` | Display ordering within category |
2275
+ | `treatment_recall_days` | Int | No | `null` | Days after completion to schedule a recall. Null = no recall. E.g., 180 for biannual cleaning |
2276
+ | `treatment_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2277
+ | `treatment_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2278
+
2279
+ **Indexes:**
2280
+
2281
+ | Index | Columns | Purpose |
2282
+ |-------|---------|---------|
2283
+ | Primary | `treatment_id` | PK lookup |
2284
+ | `idx_treatments_tenant_active` | `(treatment_tenant_id, treatment_is_active)` | Active treatment list |
2285
+ | `idx_treatments_tenant_category` | `(treatment_tenant_id, treatment_category)` | Filter by category |
2286
+ | `idx_treatments_clinic` | `treatment_clinic_id` | Clinic-scoped treatments |
2287
+
2288
+ **Relations:**
2289
+
2290
+ | Direction | Target | FK Column | Description |
2291
+ |-----------|--------|-----------|-------------|
2292
+ | belongs_to | tenants | `treatment_tenant_id` | Parent tenant (cascade delete) |
2293
+ | belongs_to | clinics | `treatment_clinic_id` | Scoped clinic (optional) |
2294
+ | has_many | treatment_plan_items | -- | Plan items referencing this treatment |
2295
+ | has_many | appointments | -- | Appointments for this treatment |
2296
+
2297
+ **Design decisions:**
2298
+ - `treatment_clinic_id` null means available at all clinics. Clinic-specific treatments (e.g., a treatment only offered at the Kadikoy branch) set the clinic FK.
2299
+ - `treatment_category` is a free-form string, not an enum. Clinics define their own categories.
2300
+ - `Decimal` for price to avoid floating-point currency issues.
2301
+ - `treatment_recall_days` supports P1 recall automation. E.g., dental cleaning = 180 days → auto-remind patient in 6 months.
2302
+
2303
+ ---
2304
+
2305
+ ### 41. treatment_plans
2306
+
2307
+ **Purpose:** A multi-visit treatment plan for a specific patient. Contains ordered items, each linked to a treatment from the catalog and optionally to a booked appointment.
2308
+
2309
+ | Column | Type | Required | Default | Description |
2310
+ |--------|------|----------|---------|-------------|
2311
+ | `tplan_id` | UUID | Yes | `uuid()` | Primary key |
2312
+ | `tplan_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2313
+ | `tplan_clinic_id` | UUID | Yes | -- | FK to `clinics` |
2314
+ | `tplan_patient_id` | UUID | Yes | -- | FK to `patients` |
2315
+ | `tplan_doctor_id` | UUID | No | `null` | FK to `users` (doctor role). Doctor managing this plan |
2316
+ | `tplan_name` | String | Yes | -- | Plan name (e.g., "Full Mouth Rehabilitation") |
2317
+ | `tplan_status` | TreatmentPlanStatus | Yes | `active` | `active`, `completed`, `cancelled` |
2318
+ | `tplan_notes` | String | Yes | `""` | **Encrypted.** Doctor notes about the plan. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2319
+ | `tplan_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2320
+ | `tplan_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2321
+
2322
+ **Indexes:**
2323
+
2324
+ | Index | Columns | Purpose |
2325
+ |-------|---------|---------|
2326
+ | Primary | `tplan_id` | PK lookup |
2327
+ | `idx_tplans_patient_status` | `(tplan_patient_id, tplan_status)` | Patient's active/completed plans |
2328
+ | `idx_tplans_tenant_status` | `(tplan_tenant_id, tplan_status)` | Tenant-wide plan overview |
2329
+ | `idx_tplans_doctor` | `tplan_doctor_id` | Doctor's plans |
2330
+ | `idx_tplans_clinic` | `tplan_clinic_id` | Clinic-scoped plans |
2331
+
2332
+ **Relations:**
2333
+
2334
+ | Direction | Target | FK Column | Description |
2335
+ |-----------|--------|-----------|-------------|
2336
+ | belongs_to | tenants | `tplan_tenant_id` | Parent tenant (cascade delete) |
2337
+ | belongs_to | clinics | `tplan_clinic_id` | Scoped clinic |
2338
+ | belongs_to | patients | `tplan_patient_id` | Patient this plan is for |
2339
+ | belongs_to | users | `tplan_doctor_id` | Doctor managing the plan |
2340
+ | has_many | treatment_plan_items | -- | Ordered items in this plan |
2341
+ | has_many | patient_photo_sets | -- | Before/after photos linked to this plan |
2342
+
2343
+ **Design decisions:**
2344
+ - No financial fields (deferred). Total amount can be computed from items when needed.
2345
+ - `tplan_clinic_id` is required. A treatment plan belongs to the clinic where the patient is being treated.
2346
+ - `tplan_doctor_id` is optional — some plans may not have a specific doctor (e.g., general clinic plan).
2347
+ - `tplan_notes` encrypted with `PATIENT_PII_ENCRYPTION_KEY` — may contain clinical reasoning.
2348
+ - Plan auto-completes when all items reach `completed` status (application logic, not DB trigger).
2349
+
2350
+ ---
2351
+
2352
+ ### 42. treatment_plan_items
2353
+
2354
+ **Purpose:** A single item within a treatment plan. Represents one procedure/visit. Linked to a treatment from the catalog and optionally to a booked appointment.
2355
+
2356
+ | Column | Type | Required | Default | Description |
2357
+ |--------|------|----------|---------|-------------|
2358
+ | `tpitem_id` | UUID | Yes | `uuid()` | Primary key |
2359
+ | `tpitem_plan_id` | UUID | Yes | -- | FK to `treatment_plans` |
2360
+ | `tpitem_treatment_id` | UUID | No | `null` | FK to `treatments`. Null for custom one-off items |
2361
+ | `tpitem_appointment_id` | UUID | No | `null` | FK to `appointments`. Linked when appointment is booked |
2362
+ | `tpitem_name` | String | Yes | -- | Treatment name (denormalized — survives treatment deletion) |
2363
+ | `tpitem_tooth_number` | String | No | `null` | Dental-specific: tooth number (e.g., "14", "21-23") |
2364
+ | `tpitem_status` | TreatmentPlanItemStatus | Yes | `planned` | `planned`, `scheduled`, `in_progress`, `completed`, `cancelled` |
2365
+ | `tpitem_notes` | String | Yes | `""` | **Encrypted.** Notes about this specific item. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2366
+ | `tpitem_sort_order` | Int | Yes | `0` | Display ordering within the plan |
2367
+ | `tpitem_completed_at` | DateTime | No | `null` | When this item was marked completed |
2368
+ | `tpitem_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2369
+ | `tpitem_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2370
+
2371
+ **Indexes:**
2372
+
2373
+ | Index | Columns | Purpose |
2374
+ |-------|---------|---------|
2375
+ | Primary | `tpitem_id` | PK lookup |
2376
+ | `idx_tpitems_plan_order` | `(tpitem_plan_id, tpitem_sort_order)` | Ordered item list within a plan |
2377
+ | `idx_tpitems_appointment` | `tpitem_appointment_id` | Find which plan item an appointment is for |
2378
+
2379
+ **Relations:**
2380
+
2381
+ | Direction | Target | FK Column | Description |
2382
+ |-----------|--------|-----------|-------------|
2383
+ | belongs_to | treatment_plans | `tpitem_plan_id` | Parent plan (cascade delete) |
2384
+ | belongs_to | treatments | `tpitem_treatment_id` | Treatment from catalog (optional) |
2385
+ | belongs_to | appointments | `tpitem_appointment_id` | Linked appointment (optional) |
2386
+
2387
+ **Design decisions:**
2388
+ - `tpitem_treatment_id` is nullable for custom/one-off items not in the catalog (e.g., "Special consultation").
2389
+ - `tpitem_name` is denormalized — if the treatment is renamed or deleted later, the plan item still shows what was planned.
2390
+ - `tpitem_notes` encrypted with `PATIENT_PII_ENCRYPTION_KEY` — may contain clinical details per procedure step.
2391
+ - `tpitem_tooth_number` is dental-specific. Aesthetic/dermatology clinics leave it null.
2392
+ - Status transitions: `planned` → `scheduled` (appointment linked) → `in_progress` → `completed`. Can be `cancelled` from any state.
2393
+ - Cascade delete from plan: deleting a plan removes all items.
2394
+
2395
+ ---
2396
+
2397
+ ### 43. doctor_profiles
2398
+
2399
+ **Purpose:** Extended profile for users with the `doctor` role. Stores specialty, working hours, and appointment configuration. One-to-one with `users`.
2400
+
2401
+ | Column | Type | Required | Default | Description |
2402
+ |--------|------|----------|---------|-------------|
2403
+ | `dprofile_id` | UUID | Yes | `uuid()` | Primary key |
2404
+ | `dprofile_user_id` | UUID | Yes (unique) | -- | FK to `users`. One-to-one |
2405
+ | `dprofile_specialty` | String | Yes | `""` | Specialty (e.g., "Ortodonti", "Implantoloji", "Estetik Cerrahi") |
2406
+ | `dprofile_title` | String | Yes | `""` | Professional title (e.g., "Dr.", "Dt.", "Prof. Dr.") |
2407
+ | `dprofile_bio` | String | Yes | `""` | Short biography / description |
2408
+ | `dprofile_working_hours` | Json | Yes | `"[]"` | Array of `{day, start, end}` objects defining availability |
2409
+ | `dprofile_appointment_duration` | Int | Yes | `30` | Default appointment slot duration in minutes |
2410
+ | `dprofile_color` | String | Yes | `""` | Calendar display color (hex, e.g., `#3B82F6`) |
2411
+ | `dprofile_is_accepting` | Boolean | Yes | `true` | Whether the doctor is accepting new patients/appointments |
2412
+ | `dprofile_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2413
+ | `dprofile_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2414
+
2415
+ **Indexes:**
2416
+
2417
+ | Index | Columns | Purpose |
2418
+ |-------|---------|---------|
2419
+ | Primary | `dprofile_id` | PK lookup |
2420
+ | Unique | `dprofile_user_id` | Enforce one-to-one with users |
2421
+
2422
+ **Relations:**
2423
+
2424
+ | Direction | Target | FK Column | Description |
2425
+ |-----------|--------|-----------|-------------|
2426
+ | belongs_to | users | `dprofile_user_id` | Parent user (cascade delete) |
2427
+
2428
+ **Design decisions:**
2429
+ - One-to-one with User. Created automatically when a user with `doctor` role is added.
2430
+ - `profile_working_hours` is JSONB because it is a variable-length array of per-day windows.
2431
+ - `profile_appointment_duration` overrides the clinic default when this doctor is selected for an appointment.
2432
+ - `profile_color` enables the frontend calendar to show each doctor's appointments in a distinct color.
2433
+ - `profile_is_accepting` allows temporarily stopping new bookings for a doctor without deactivating their user account.
2434
+
2435
+ ---
2436
+
2437
+ ### 44. patient_photo_sets
2438
+
2439
+ **Purpose:** A group of related photos taken at a point in time for a patient. Used for before/after documentation of treatments. Each set contains multiple photos (different angles).
2440
+
2441
+ | Column | Type | Required | Default | Description |
2442
+ |--------|------|----------|---------|-------------|
2443
+ | `photoset_id` | UUID | Yes | `uuid()` | Primary key |
2444
+ | `photoset_patient_id` | UUID | Yes | -- | FK to `patients` |
2445
+ | `photoset_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2446
+ | `photoset_clinic_id` | UUID | Yes | -- | FK to `clinics` |
2447
+ | `photoset_treatment_plan_id` | UUID | No | `null` | FK to `treatment_plans`. Optional link to a treatment plan |
2448
+ | `photoset_type` | String | Yes | -- | `before`, `after_1week`, `after_1month`, `after_3months`, `progress`, `other` |
2449
+ | `photoset_label` | String | Yes | `""` | Free-form label (e.g., "Pre-op", "6 ay sonrasi") |
2450
+ | `photoset_taken_at` | DateTime | Yes | `now()` | When the photos were taken (not uploaded) |
2451
+ | `photoset_taken_by` | UUID | No | `null` | FK to `users`. Who took the photos |
2452
+ | `photoset_notes` | String | Yes | `""` | **Encrypted.** Notes about this photo set. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2453
+ | `photoset_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2454
+ | `photoset_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2455
+
2456
+ **Indexes:**
2457
+
2458
+ | Index | Columns | Purpose |
2459
+ |-------|---------|---------|
2460
+ | Primary | `photoset_id` | PK lookup |
2461
+ | `idx_photosets_patient_date` | `(photoset_patient_id, photoset_taken_at DESC)` | Patient photo timeline |
2462
+ | `idx_photosets_plan` | `photoset_treatment_plan_id` | Photos for a treatment plan |
2463
+ | `idx_photosets_tenant_type` | `(photoset_tenant_id, photoset_type)` | Filter by type across tenant |
2464
+
2465
+ **Relations:**
2466
+
2467
+ | Direction | Target | FK Column | Description |
2468
+ |-----------|--------|-----------|-------------|
2469
+ | belongs_to | patients | `photoset_patient_id` | Parent patient (cascade delete) |
2470
+ | belongs_to | tenants | `photoset_tenant_id` | Parent tenant |
2471
+ | belongs_to | clinics | `photoset_clinic_id` | Scoped clinic |
2472
+ | belongs_to | treatment_plans | `photoset_treatment_plan_id` | Linked treatment plan (optional) |
2473
+ | belongs_to | users | `photoset_taken_by` | Photographer |
2474
+ | has_many | patient_photos | -- | Individual photos in this set |
2475
+
2476
+ **Design decisions:**
2477
+ - `photoset_taken_at` is separate from `created_at` — photos may be uploaded days after they were taken.
2478
+ - `photoset_type` is a string, not a Prisma enum — clinics may define custom photo set types.
2479
+ - `photoset_treatment_plan_id` enables grouping before/after photos by treatment plan for comparison views.
2480
+ - Cascade delete from patient: deleting a patient removes all photo sets and photos (+ MinIO cleanup).
2481
+
2482
+ ---
2483
+
2484
+ ### 45. patient_photos
2485
+
2486
+ **Purpose:** Individual photos within a photo set. Each photo has an angle tag for structured comparison (front-to-front, left-to-left). Stored in MinIO.
2487
+
2488
+ | Column | Type | Required | Default | Description |
2489
+ |--------|------|----------|---------|-------------|
2490
+ | `photo_id` | UUID | Yes | `uuid()` | Primary key |
2491
+ | `photo_set_id` | UUID | Yes | -- | FK to `patient_photo_sets` |
2492
+ | `photo_angle` | String | Yes | `""` | Photo angle: `front`, `left`, `right`, `top`, `bottom`, `smile`, `closeup`, `other` |
2493
+ | `photo_media_key` | String | Yes | -- | MinIO object key: `{tenantId}/patients/{patientId}/photos/{setId}/{photoId}.{ext}` |
2494
+ | `photo_thumbnail_key` | String | No | `null` | MinIO key for thumbnail (generated on upload) |
2495
+ | `photo_caption` | String | Yes | `""` | Optional caption |
2496
+ | `photo_sort_order` | Int | Yes | `0` | Display ordering within the set |
2497
+ | `photo_created_at` | DateTime | Yes | `now()` | Upload timestamp |
2498
+
2499
+ **Indexes:**
2500
+
2501
+ | Index | Columns | Purpose |
2502
+ |-------|---------|---------|
2503
+ | Primary | `photo_id` | PK lookup |
2504
+ | `idx_photos_set_order` | `(photo_set_id, photo_sort_order)` | Ordered photos within a set |
2505
+
2506
+ **Relations:**
2507
+
2508
+ | Direction | Target | FK Column | Description |
2509
+ |-----------|--------|-----------|-------------|
2510
+ | belongs_to | patient_photo_sets | `photo_set_id` | Parent photo set (cascade delete) |
2511
+
2512
+ **Design decisions:**
2513
+ - No `photo_updated_at`. Photos are immutable once uploaded. To replace, delete and re-upload.
2514
+ - `photo_angle` enables the frontend to align before/after photos by matching angles in comparison grids.
2515
+ - `photo_thumbnail_key` stores a pre-generated thumbnail (resized on upload) for fast list rendering without loading full images.
2516
+ - Cascade delete from photo set: deleting a set removes all photos (DB records). MinIO cleanup is handled by application code.
2517
+ - Served via signed URLs only. No public access.
2518
+
2519
+ ---
2520
+
2521
+ ### 46. examination_records
2522
+
2523
+ **Purpose:** Per-visit clinical record. What the doctor found, diagnosed, performed, and prescribed during an appointment. One-to-one with `appointments`. Created when a doctor completes a visit.
2524
+
2525
+ | Column | Type | Required | Default | Description |
2526
+ |--------|------|----------|---------|-------------|
2527
+ | `exam_id` | UUID | Yes | `uuid()` | Primary key |
2528
+ | `exam_appointment_id` | UUID | Yes (unique) | -- | FK to `appointments`. One-to-one |
2529
+ | `exam_patient_id` | UUID | Yes | -- | FK to `patients`. Denormalized for direct patient queries |
2530
+ | `exam_doctor_id` | UUID | Yes | -- | FK to `users` (doctor role). Who performed the examination |
2531
+ | `exam_clinic_id` | UUID | Yes | -- | FK to `clinics` |
2532
+ | `exam_tenant_id` | UUID | Yes | -- | FK to `tenants` |
2533
+ | `exam_findings` | String | Yes | `""` | **Encrypted.** What the doctor observed. AES-256-GCM via `PATIENT_PII_ENCRYPTION_KEY` |
2534
+ | `exam_diagnosis` | String | Yes | `""` | **Encrypted.** Diagnosis. Same key |
2535
+ | `exam_procedure_notes` | String | Yes | `""` | **Encrypted.** What was done during the visit. Same key |
2536
+ | `exam_prescription` | String | Yes | `""` | **Encrypted.** Medications prescribed. Same key |
2537
+ | `exam_instructions` | String | Yes | `""` | **Encrypted.** Follow-up instructions for the patient. Same key |
2538
+ | `exam_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
2539
+ | `exam_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
2540
+
2541
+ **Indexes:**
2542
+
2543
+ | Index | Columns | Purpose |
2544
+ |-------|---------|---------|
2545
+ | Primary | `exam_id` | PK lookup |
2546
+ | Unique | `exam_appointment_id` | Enforce one-to-one with appointments |
2547
+ | `idx_exam_patient_date` | `(exam_patient_id, exam_created_at DESC)` | Patient examination history |
2548
+ | `idx_exam_doctor_date` | `(exam_doctor_id, exam_created_at DESC)` | Doctor's examination history |
2549
+
2550
+ **Relations:**
2551
+
2552
+ | Direction | Target | FK Column | Description |
2553
+ |-----------|--------|-----------|-------------|
2554
+ | belongs_to | appointments | `exam_appointment_id` | Which appointment this exam is for (cascade delete) |
2555
+ | belongs_to | patients | `exam_patient_id` | Patient examined |
2556
+ | belongs_to | users (doctor) | `exam_doctor_id` | Doctor who performed the exam |
2557
+ | belongs_to | clinics | `exam_clinic_id` | Which clinic |
2558
+ | belongs_to | tenants | `exam_tenant_id` | Parent tenant (cascade delete) |
2559
+
2560
+ **Design decisions:**
2561
+ - **All clinical text fields are encrypted** with `PATIENT_PII_ENCRYPTION_KEY`. Findings, diagnosis, procedure notes, prescription, and instructions are sensitive health data.
2562
+ - One-to-one with appointments. Not every appointment has an exam record (e.g., no-shows, cancelled). The record is created when the doctor fills in the clinical form after the visit.
2563
+ - `exam_patient_id` is denormalized (could be resolved via appointment → patient). Direct FK enables efficient patient timeline queries without joining through appointments.
2564
+ - This is the **per-visit record**. The static background health info (allergies, conditions) stays in `patient_medical_histories` (#37).