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,1034 @@
1
+ # Patients
2
+
3
+ > Central entity for the clinic management pivot. Everything links back to Patient:
4
+ > appointments, conversations, calls, treatment plans, photos, documents, notes.
5
+ > Replaces `WhatsAppContactProfile` as the core identity.
6
+ > Last updated: 2026-03-29
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Overview](#1-overview)
13
+ 2. [Patient Model](#2-patient-model)
14
+ 3. [Medical History (Anamnesis)](#3-medical-history-anamnesis)
15
+ 4. [Patient Documents](#4-patient-documents)
16
+ 5. [Patient Notes](#5-patient-notes)
17
+ 6. [Patient Timeline](#6-patient-timeline)
18
+ 7. [Auto-Creation from AI Contact](#7-auto-creation-from-ai-contact)
19
+ 8. [API Endpoints](#8-api-endpoints)
20
+ 9. [Permissions](#9-permissions)
21
+ 10. [Patient Merge (Duplicate Resolution)](#10-patient-merge-duplicate-resolution)
22
+ 11. [WhatsAppContactProfile Migration](#11-whatsappcontactprofile-migration)
23
+ 12. [NestJS Module Structure](#12-nestjs-module-structure)
24
+ 13. [Feature Flag](#13-feature-flag)
25
+ 14. [Affected Existing Models](#14-affected-existing-models)
26
+
27
+ ---
28
+
29
+ ## 1. Overview
30
+
31
+ The product pivot from pure AI receptionist to full clinic management tool makes Patient the central entity in the data model. Every piece of data ultimately connects back to a patient:
32
+
33
+ ```
34
+ Patient
35
+ |
36
+ +-- Appointments (booked, cancelled, completed, no_show)
37
+ +-- WhatsApp Chats (conversations with AI and staff)
38
+ +-- Calls (inbound, outbound, with summaries)
39
+ +-- Operator Requests (photo reviews forwarded to doctor)
40
+ +-- Treatment Plans (planned and completed procedures)
41
+ +-- Photo Sets (before/after clinical photos)
42
+ +-- Documents (consent forms, lab results, x-rays)
43
+ +-- Notes (staff observations, pinned items)
44
+ +-- Tasks (follow-ups, reminders)
45
+ +-- Medical History (allergies, conditions, medications)
46
+ ```
47
+
48
+ **Key principles:**
49
+
50
+ - **Auto-created on first AI contact.** When a patient sends a WhatsApp message, makes a voice call, or sends an Instagram DM, a Patient record is created automatically with whatever information is available (phone number, display name from the messaging platform).
51
+ - **Staff enriches from dashboard.** Demographics, medical history, documents, and tags are filled in by clinic staff over time.
52
+ - **Replaces WhatsAppContactProfile.** The existing `whatsapp_contact_profiles` table is superseded. All its data migrates into `patients` and related tables. See [Section 11](#11-whatsappcontactprofile-migration).
53
+ - **Multi-channel identity.** A patient is identified primarily by phone number (E.164) within a tenant. The same person contacting via WhatsApp and voice call is the same Patient record.
54
+
55
+ ---
56
+
57
+ ## 2. Patient Model
58
+
59
+ ### Prisma Schema
60
+
61
+ ```prisma
62
+ model Patient {
63
+ patient_id String @id @default(uuid())
64
+ patient_tenant_id String
65
+ patient_clinic_id String? // Primary clinic
66
+ patient_phone String // E.164, primary identifier
67
+ patient_email String?
68
+ patient_first_name String
69
+ patient_last_name String
70
+ patient_display_name String @default("") // Auto-built or manual
71
+ patient_date_of_birth DateTime?
72
+ patient_gender String? // male | female | other
73
+ patient_id_number String? // TC Kimlik No
74
+ patient_blood_type String?
75
+ patient_preferred_language String @default("tr")
76
+ patient_source String? // instagram_ad | google | referral | walk_in | whatsapp | voice_call | campaign
77
+ patient_source_detail String? // Which campaign, referrer name, ad ID
78
+ patient_tags String[] @default([])
79
+ patient_ai_summary String @default("") // AI-generated from conversations
80
+ patient_is_active Boolean @default(true)
81
+ patient_first_contact_at DateTime @default(now())
82
+ patient_last_contact_at DateTime @default(now())
83
+ patient_total_sessions Int @default(0)
84
+ patient_total_appointments Int @default(0)
85
+ patient_created_at DateTime @default(now())
86
+ patient_updated_at DateTime @updatedAt
87
+
88
+ tenant Tenant @relation(fields: [patient_tenant_id], references: [tenant_id], onDelete: Cascade)
89
+ clinic Clinic? @relation(fields: [patient_clinic_id], references: [clinic_id])
90
+ medicalHistory PatientMedicalHistory?
91
+ treatmentPlans TreatmentPlan[]
92
+ appointments Appointment[]
93
+ photoSets PatientPhotoSet[]
94
+ documents PatientDocument[]
95
+ notes PatientNote[]
96
+ chats WhatsAppChat[]
97
+ operatorRequests OperatorRequest[]
98
+ tasks Task[]
99
+
100
+ @@unique([patient_tenant_id, patient_phone])
101
+ @@index([patient_tenant_id, patient_last_contact_at(sort: Desc)])
102
+ @@index([patient_clinic_id])
103
+ @@index([patient_tenant_id, patient_last_name, patient_first_name])
104
+ @@map("patients")
105
+ }
106
+ ```
107
+
108
+ ### Column Descriptions
109
+
110
+ | Column | Type | Required | Default | Description |
111
+ |--------|------|----------|---------|-------------|
112
+ | `patient_id` | UUID | Yes | `uuid()` | Primary key |
113
+ | `patient_tenant_id` | UUID | Yes | -- | FK to `tenants` |
114
+ | `patient_clinic_id` | UUID | No | `null` | FK to `clinics`. Primary clinic for this patient. Null if not yet assigned (auto-created patients start unassigned, routed clinic is set on creation if known) |
115
+ | `patient_phone` | String | Yes | -- | Phone number in E.164 format (e.g., `+905551234567`). Primary identifier for cross-channel matching |
116
+ | `patient_email` | String | No | `null` | Email address. Optional, entered by staff |
117
+ | `patient_first_name` | String | Yes | -- | First name. May be extracted from display name on auto-creation |
118
+ | `patient_last_name` | String | Yes | -- | Last name. May be `"Bilinmiyor"` (Unknown) on auto-creation |
119
+ | `patient_display_name` | String | Yes | `""` | Display name shown in UI. Auto-built from first + last name, or manually overridden. On auto-creation, populated from WhatsApp push name or contact info |
120
+ | `patient_date_of_birth` | DateTime | No | `null` | Date of birth. Entered by staff or collected by AI |
121
+ | `patient_gender` | String | No | `null` | Gender: `male`, `female`, or `other`. String rather than enum to avoid migration for future values |
122
+ | `patient_id_number` | String | No | `null` | Turkish national ID number (TC Kimlik No). 11 digits. Used for government reporting |
123
+ | `patient_blood_type` | String | No | `null` | Blood type (e.g., `A+`, `B-`, `0+`, `AB-`). Relevant for surgical clinics |
124
+ | `patient_preferred_language` | String | Yes | `"tr"` | ISO 639-1 language code. Determines AI agent language selection |
125
+ | `patient_source` | String | No | `null` | How the patient first reached the clinic. Values: `instagram_ad`, `google`, `referral`, `walk_in`, `whatsapp`, `voice_call`, `campaign`. String rather than enum because clinics will add custom sources |
126
+ | `patient_source_detail` | String | No | `null` | Free text detail about the source. Examples: campaign name, referrer's name, specific ad ID, Google Ads keyword |
127
+ | `patient_tags` | String[] | Yes | `[]` | Tags for categorization (e.g., `["dental", "vip", "returning"]`). Array rather than junction table -- tags are simple text labels, not referential entities |
128
+ | `patient_ai_summary` | String | Yes | `""` | AI-generated summary aggregated from all conversations. Regenerated asynchronously (BullMQ job) after each session resolves |
129
+ | `patient_is_active` | Boolean | Yes | `true` | Soft-delete flag. `false` = patient is archived/deleted. Filtered out of default queries |
130
+ | `patient_first_contact_at` | DateTime | Yes | `now()` | Timestamp of the patient's first interaction with the clinic (any channel) |
131
+ | `patient_last_contact_at` | DateTime | Yes | `now()` | Timestamp of the patient's most recent interaction. Updated on every message, call, or appointment |
132
+ | `patient_total_sessions` | Int | Yes | `0` | Denormalized counter of WhatsApp sessions. Incremented on session create. Avoids COUNT queries on the patient list page |
133
+ | `patient_total_appointments` | Int | Yes | `0` | Denormalized counter of appointments (all statuses). Incremented on appointment create |
134
+ | `patient_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
135
+ | `patient_updated_at` | DateTime | Yes | auto | Automatically updated on every write via Prisma `@updatedAt` |
136
+
137
+ ### Indexes
138
+
139
+ | Index | Columns | Purpose |
140
+ |-------|---------|---------|
141
+ | Primary | `patient_id` | PK lookup |
142
+ | Unique | `(patient_tenant_id, patient_phone)` | One patient per phone per tenant. The core deduplication constraint |
143
+ | `idx_patients_tenant_lastcontact` | `(patient_tenant_id, patient_last_contact_at DESC)` | Patient list sorted by most recent contact (default sort) |
144
+ | `idx_patients_clinic` | `(patient_clinic_id)` | Filter patients by primary clinic |
145
+ | `idx_patients_tenant_name` | `(patient_tenant_id, patient_last_name, patient_first_name)` | Alphabetical patient list, name search |
146
+
147
+ ### Relations
148
+
149
+ | Direction | Target | FK Column | Description |
150
+ |-----------|--------|-----------|-------------|
151
+ | belongs_to | tenants | `patient_tenant_id` | Parent tenant (cascade delete) |
152
+ | belongs_to | clinics | `patient_clinic_id` | Primary clinic (nullable) |
153
+ | has_one | patient_medical_histories | -- | Medical history / anamnesis |
154
+ | has_many | treatment_plans | -- | Treatment plans |
155
+ | has_many | appointments | -- | All appointments (via `appt_patient_id`) |
156
+ | has_many | patient_photo_sets | -- | Clinical photo sets |
157
+ | has_many | patient_documents | -- | Uploaded documents |
158
+ | has_many | patient_notes | -- | Staff notes |
159
+ | has_many | whatsapp_chats | -- | WhatsApp conversations (via `chat_patient_id`) |
160
+ | has_many | operator_requests | -- | Operator workflow requests (via session -> chat -> patient) |
161
+ | has_many | tasks | -- | Follow-up tasks |
162
+
163
+ ### Design Decisions
164
+
165
+ - **`patient_phone` as primary identifier, not `patient_id_number`.** TC Kimlik is optional -- many patients never provide it. Phone number is always available because it is the contact method. The `(tenant_id, phone)` unique constraint is the deduplication key.
166
+ - **`patient_clinic_id` is nullable.** Auto-created patients may not have a known primary clinic if the routing context is ambiguous. Staff assigns the clinic later. For single-clinic tenants, it is set to the default clinic on creation.
167
+ - **`patient_source` is String, not Prisma enum.** Clinics will want custom acquisition sources (specific campaigns, partner names). Adding a value should not require a database migration.
168
+ - **`patient_gender` is String, not Prisma enum.** Same rationale. The common values are `male`, `female`, `other`, but the field is not constrained at the DB level.
169
+ - **`patient_tags` is `String[]`, not a junction table.** Tags are simple text labels used for filtering and display. They are not entities with their own properties. Array containment queries (`@>`) are sufficient.
170
+ - **Denormalized counters (`patient_total_sessions`, `patient_total_appointments`).** The patient list page shows these counts. Running `COUNT(*)` with JOINs for every row in a paginated list is expensive. Counters are incremented atomically on session/appointment creation.
171
+ - **`patient_display_name` is separate from `first_name` + `last_name`.** Auto-created patients often have only a WhatsApp push name (e.g., "Mehmet A.") that does not split cleanly into first and last. The display name stores whatever the system has. Staff can override it later.
172
+
173
+ ---
174
+
175
+ ## 3. Medical History (Anamnesis)
176
+
177
+ ### Prisma Schema
178
+
179
+ ```prisma
180
+ model PatientMedicalHistory {
181
+ history_id String @id @default(uuid())
182
+ history_patient_id String @unique
183
+ history_allergies String[] @default([])
184
+ history_chronic_conditions String[] @default([])
185
+ history_current_medications String[] @default([])
186
+ history_past_surgeries String @default("")
187
+ history_pregnancy_status String? // not_applicable | not_pregnant | pregnant | unknown
188
+ history_smoking Boolean?
189
+ history_alcohol Boolean?
190
+ history_notes String @default("")
191
+ history_anamnesis_completed Boolean @default(false)
192
+ history_anamnesis_date DateTime?
193
+ history_created_at DateTime @default(now())
194
+ history_updated_at DateTime @updatedAt
195
+
196
+ patient Patient @relation(fields: [history_patient_id], references: [patient_id], onDelete: Cascade)
197
+
198
+ @@map("patient_medical_histories")
199
+ }
200
+ ```
201
+
202
+ ### Column Descriptions
203
+
204
+ | Column | Type | Required | Default | Description |
205
+ |--------|------|----------|---------|-------------|
206
+ | `history_id` | UUID | Yes | `uuid()` | Primary key |
207
+ | `history_patient_id` | UUID | Yes | -- | FK to `patients`. One-to-one (unique constraint) |
208
+ | `history_allergies` | String[] | Yes | `[]` | Known allergies (e.g., `["penisilin", "lateks"]`). Free text array |
209
+ | `history_chronic_conditions` | String[] | Yes | `[]` | Chronic conditions (e.g., `["diyabet", "hipertansiyon"]`). Free text array |
210
+ | `history_current_medications` | String[] | Yes | `[]` | Medications currently being taken (e.g., `["metformin 500mg", "aspirin 100mg"]`) |
211
+ | `history_past_surgeries` | String | Yes | `""` | Free text description of past surgical procedures |
212
+ | `history_pregnancy_status` | String | No | `null` | Pregnancy status: `not_applicable`, `not_pregnant`, `pregnant`, `unknown`. Relevant for dental/cosmetic procedures (anesthesia, X-ray contraindications) |
213
+ | `history_smoking` | Boolean | No | `null` | Whether the patient smokes. Affects healing timelines and treatment plans. `null` = not asked |
214
+ | `history_alcohol` | Boolean | No | `null` | Whether the patient consumes alcohol regularly. `null` = not asked |
215
+ | `history_notes` | String | Yes | `""` | Free text notes from the doctor about the patient's medical background |
216
+ | `history_anamnesis_completed` | Boolean | Yes | `false` | Whether a doctor has completed the anamnesis form. Controls UI state (show "incomplete" badge) |
217
+ | `history_anamnesis_date` | DateTime | No | `null` | When the anamnesis was completed. Set when `history_anamnesis_completed` transitions to `true` |
218
+ | `history_created_at` | DateTime | Yes | `now()` | Record creation timestamp |
219
+ | `history_updated_at` | DateTime | Yes | auto | Automatically updated on every write |
220
+
221
+ ### Indexes
222
+
223
+ | Index | Columns | Purpose |
224
+ |-------|---------|---------|
225
+ | Primary | `history_id` | PK lookup |
226
+ | Unique | `history_patient_id` | 1:1 relationship enforcement |
227
+
228
+ ### Relations
229
+
230
+ | Direction | Target | FK Column | Description |
231
+ |-----------|--------|-----------|-------------|
232
+ | belongs_to | patients | `history_patient_id` | Parent patient (cascade delete) |
233
+
234
+ ### Design Decisions
235
+
236
+ - **Separate table, not JSONB.** Medical history is a distinct clinical concern, queried independently from patient demographics. Turkish healthcare regulations (KVKK, Saglik Bakanligi) may require specific field-level audit trails. A dedicated table makes this straightforward.
237
+ - **Created empty on patient creation.** The `PatientMedicalHistory` record is always created alongside the Patient (even for auto-created patients). This avoids null-checking in every medical history read -- the row always exists, just with defaults.
238
+ - **Arrays for allergies, conditions, medications.** These are free-text lists, not coded medical terminologies (ICD-10, ATC). Turkish clinics primarily use free text. If coded terminology is needed later, it can be added as a parallel field without breaking existing data.
239
+ - **`history_pregnancy_status` is String.** The values are a small fixed set but stored as String to avoid an enum migration for edge cases (e.g., `postpartum`).
240
+
241
+ ---
242
+
243
+ ## 4. Patient Documents
244
+
245
+ ### Prisma Schema
246
+
247
+ ```prisma
248
+ model PatientDocument {
249
+ doc_id String @id @default(uuid())
250
+ doc_patient_id String
251
+ doc_tenant_id String
252
+ doc_type String // consent_form | lab_result | x_ray | prescription | referral | other
253
+ doc_name String
254
+ doc_media_key String // MinIO key: {tenantId}/patients/{patientId}/documents/{docId}.{ext}
255
+ doc_content_type String // MIME type
256
+ doc_size_bytes Int @default(0)
257
+ doc_uploaded_by String? // FK to users
258
+ doc_notes String @default("")
259
+ doc_created_at DateTime @default(now())
260
+
261
+ patient Patient @relation(fields: [doc_patient_id], references: [patient_id], onDelete: Cascade)
262
+ tenant Tenant @relation(fields: [doc_tenant_id], references: [tenant_id], onDelete: Cascade)
263
+ uploadedBy User? @relation(fields: [doc_uploaded_by], references: [user_id])
264
+
265
+ @@index([doc_patient_id, doc_created_at(sort: Desc)])
266
+ @@index([doc_tenant_id, doc_type])
267
+ @@map("patient_documents")
268
+ }
269
+ ```
270
+
271
+ ### Column Descriptions
272
+
273
+ | Column | Type | Required | Default | Description |
274
+ |--------|------|----------|---------|-------------|
275
+ | `doc_id` | UUID | Yes | `uuid()` | Primary key |
276
+ | `doc_patient_id` | UUID | Yes | -- | FK to `patients` |
277
+ | `doc_tenant_id` | UUID | Yes | -- | FK to `tenants`. Denormalized for tenant-level queries (e.g., "all X-rays for this tenant") |
278
+ | `doc_type` | String | Yes | -- | Document type: `consent_form`, `lab_result`, `x_ray`, `prescription`, `referral`, `other`. String rather than Prisma enum because clinics will want custom types |
279
+ | `doc_name` | String | Yes | -- | Human-readable document name (e.g., "Panoramik Rontgen 2026-03-15") |
280
+ | `doc_media_key` | String | Yes | -- | MinIO object key. Format: `{tenantId}/patients/{patientId}/documents/{docId}.{ext}`. Never exposed to frontend -- served via signed URLs |
281
+ | `doc_content_type` | String | Yes | -- | MIME type (e.g., `image/jpeg`, `application/pdf`, `image/png`) |
282
+ | `doc_size_bytes` | Int | Yes | `0` | File size in bytes. Used for storage quota calculations and display |
283
+ | `doc_uploaded_by` | UUID | No | `null` | FK to `users`. The staff member who uploaded the document. Null for system-generated documents |
284
+ | `doc_notes` | String | Yes | `""` | Free text notes about the document (e.g., "Left molar area") |
285
+ | `doc_created_at` | DateTime | Yes | `now()` | Upload timestamp. No `updated_at` -- documents are immutable once uploaded (delete and re-upload to replace) |
286
+
287
+ ### Indexes
288
+
289
+ | Index | Columns | Purpose |
290
+ |-------|---------|---------|
291
+ | Primary | `doc_id` | PK lookup |
292
+ | `idx_docs_patient_created` | `(doc_patient_id, doc_created_at DESC)` | Patient document list sorted by recency |
293
+ | `idx_docs_tenant_type` | `(doc_tenant_id, doc_type)` | Filter all documents of a type for a tenant |
294
+
295
+ ### Relations
296
+
297
+ | Direction | Target | FK Column | Description |
298
+ |-----------|--------|-----------|-------------|
299
+ | belongs_to | patients | `doc_patient_id` | Parent patient (cascade delete) |
300
+ | belongs_to | tenants | `doc_tenant_id` | Parent tenant (cascade delete) |
301
+ | belongs_to | users | `doc_uploaded_by` | Staff member who uploaded (nullable) |
302
+
303
+ ### Design Decisions
304
+
305
+ - **Stored in MinIO, not in the database.** Following the architecture rule from [18-FILE-STORAGE.md](18-FILE-STORAGE.md): no base64 in database. The database stores only the MinIO object key. All access is through signed URLs with 1-hour TTL.
306
+ - **MinIO path format:** `{tenantId}/patients/{patientId}/documents/{docId}.{ext}`. This extends the bucket structure defined in the file storage doc.
307
+ - **`doc_type` is String, not Prisma enum.** Clinics across different specialties need different document types. A dermatology clinic might add `dermoscopy_image`, an orthodontist might add `ceph_analysis`. Adding types should not require schema migrations.
308
+ - **No `updated_at`.** Documents are immutable after upload. To "update" a document, delete the old one and upload a new version. This keeps the MinIO lifecycle simple and avoids partial-update edge cases.
309
+ - **`doc_uploaded_by` nullable.** System-generated documents (e.g., auto-saved WhatsApp media promoted to documents) have no human uploader.
310
+
311
+ ### Upload Flow
312
+
313
+ ```
314
+ Staff clicks "Upload Document" on patient detail page
315
+ |
316
+ v
317
+ Frontend: POST /patients/:id/documents (multipart/form-data)
318
+ | Fields: file, doc_type, doc_name, doc_notes
319
+ v
320
+ Backend (DocumentsService):
321
+ 1. Validate file type and size (max 20MB)
322
+ 2. Generate doc_id (UUID)
323
+ 3. Construct MinIO key: {tenantId}/patients/{patientId}/documents/{docId}.{ext}
324
+ 4. Upload to MinIO: minioService.putObject(key, buffer, contentType)
325
+ 5. Create PatientDocument row in DB
326
+ 6. Return document record with signed URL for immediate display
327
+ ```
328
+
329
+ ### Serving Documents
330
+
331
+ ```
332
+ Frontend requests document list: GET /patients/:id/documents
333
+ |
334
+ v
335
+ Backend:
336
+ 1. Query PatientDocument rows for patient
337
+ 2. For each document, generate signed URL: minioService.presignedGetObject(doc_media_key, 3600)
338
+ 3. Return documents with signed URLs
339
+ |
340
+ v
341
+ Frontend displays documents with time-limited URLs (1-hour TTL)
342
+ ```
343
+
344
+ ---
345
+
346
+ ## 5. Patient Notes
347
+
348
+ ### Prisma Schema
349
+
350
+ ```prisma
351
+ model PatientNote {
352
+ note_id String @id @default(uuid())
353
+ note_patient_id String
354
+ note_tenant_id String
355
+ note_author_id String
356
+ note_text String
357
+ note_is_pinned Boolean @default(false)
358
+ note_created_at DateTime @default(now())
359
+ note_updated_at DateTime @updatedAt
360
+
361
+ patient Patient @relation(fields: [note_patient_id], references: [patient_id], onDelete: Cascade)
362
+ tenant Tenant @relation(fields: [note_tenant_id], references: [tenant_id], onDelete: Cascade)
363
+ author User @relation(fields: [note_author_id], references: [user_id])
364
+
365
+ @@index([note_patient_id, note_created_at(sort: Desc)])
366
+ @@map("patient_notes")
367
+ }
368
+ ```
369
+
370
+ ### Column Descriptions
371
+
372
+ | Column | Type | Required | Default | Description |
373
+ |--------|------|----------|---------|-------------|
374
+ | `note_id` | UUID | Yes | `uuid()` | Primary key |
375
+ | `note_patient_id` | UUID | Yes | -- | FK to `patients` |
376
+ | `note_tenant_id` | UUID | Yes | -- | FK to `tenants`. Denormalized for tenant-level queries |
377
+ | `note_author_id` | UUID | Yes | -- | FK to `users`. The staff member who wrote the note. Required -- every note has an author |
378
+ | `note_text` | String | Yes | -- | Note content. Plain text, no rich formatting |
379
+ | `note_is_pinned` | Boolean | Yes | `false` | Pinned notes appear at the top of the patient detail notes section. Useful for critical information (e.g., "patient is allergic to anesthesia") |
380
+ | `note_created_at` | DateTime | Yes | `now()` | When the note was written |
381
+ | `note_updated_at` | DateTime | Yes | auto | Automatically updated on edit |
382
+
383
+ ### Indexes
384
+
385
+ | Index | Columns | Purpose |
386
+ |-------|---------|---------|
387
+ | Primary | `note_id` | PK lookup |
388
+ | `idx_notes_patient_created` | `(note_patient_id, note_created_at DESC)` | Note list sorted by recency |
389
+
390
+ ### Relations
391
+
392
+ | Direction | Target | FK Column | Description |
393
+ |-----------|--------|-----------|-------------|
394
+ | belongs_to | patients | `note_patient_id` | Parent patient (cascade delete) |
395
+ | belongs_to | tenants | `note_tenant_id` | Parent tenant (cascade delete) |
396
+ | belongs_to | users | `note_author_id` | Note author |
397
+
398
+ ### Design Decisions
399
+
400
+ - **Author is required.** Every note must have an author. The author's name is resolved via JOIN at query time (not denormalized) because note lists are small per patient and the JOIN is cheap.
401
+ - **No separate "note type" field.** Notes are simple text entries. If structured clinical notes are needed later (e.g., SOAP format), that would be a different model (`ClinicalNote`), not an overloading of this one.
402
+ - **Pinned notes.** A simple boolean flag. The patient detail UI orders notes as: pinned notes first (by created_at DESC), then unpinned notes (by created_at DESC). No limit on pinned notes -- trust the staff.
403
+
404
+ ---
405
+
406
+ ## 6. Patient Timeline
407
+
408
+ The timeline is **not a separate table**. It is a computed view that merges multiple data sources into a single chronological feed.
409
+
410
+ ### 6.1 Timeline Sources
411
+
412
+ ```
413
+ Patient Timeline = UNION ALL of:
414
+ - Appointments (booked, cancelled, completed, no_show)
415
+ - WhatsApp sessions (created, resolved, messages)
416
+ - Calls (inbound, outbound, with summaries)
417
+ - Operator requests (forwarded, responded, expired)
418
+ - Treatment plans (created, item completed)
419
+ - Documents uploaded
420
+ - Notes added
421
+ - Photo sets added
422
+ - Tasks (created, completed)
423
+
424
+ Sorted by timestamp DESC, paginated.
425
+ ```
426
+
427
+ ### 6.2 Timeline Entry Shape
428
+
429
+ Each timeline entry is normalized to a common shape before returning to the frontend:
430
+
431
+ ```typescript
432
+ interface TimelineEntry {
433
+ id: string; // Source record ID
434
+ type: TimelineType; // 'appointment' | 'session' | 'call' | 'operator_request' | ...
435
+ timestamp: Date; // Event timestamp (used for sorting)
436
+ title: string; // Human-readable title (e.g., "Randevu - 15 Mart 10:00")
437
+ description: string; // Summary or snippet
438
+ metadata: Record<string, any>; // Type-specific data for the frontend to render
439
+ actor?: { // Who performed the action (null for system events)
440
+ id: string;
441
+ name: string;
442
+ role: string;
443
+ };
444
+ }
445
+
446
+ type TimelineType =
447
+ | 'appointment'
448
+ | 'session'
449
+ | 'call'
450
+ | 'operator_request'
451
+ | 'treatment_plan'
452
+ | 'document'
453
+ | 'note'
454
+ | 'photo_set'
455
+ | 'task';
456
+ ```
457
+
458
+ ### 6.3 Implementation
459
+
460
+ The timeline is assembled by `TimelineService`, which runs parallel queries against multiple tables and merges the results in memory:
461
+
462
+ ```
463
+ GET /patients/:id/timeline?page=1&limit=20&types=appointment,call,session
464
+ |
465
+ v
466
+ TimelineService.getTimeline(patientId, { page, limit, types })
467
+ |
468
+ v
469
+ 1. Determine which sources to query (from "types" filter, default = all)
470
+ 2. Run queries in parallel (Promise.all):
471
+ - appointmentsRepo.findByPatient(patientId, { take: limit + 1, orderBy: start_time DESC })
472
+ - whatsappSessionsRepo.findByPatient(patientId, { take: limit + 1, orderBy: created_at DESC })
473
+ - callsRepo.findByPatientPhone(patientPhone, tenantId, { take: limit + 1, orderBy: created_at DESC })
474
+ - operatorRequestsRepo.findByPatient(patientId, { take: limit + 1, orderBy: created_at DESC })
475
+ - documentsRepo.findByPatient(patientId, { take: limit + 1, orderBy: created_at DESC })
476
+ - notesRepo.findByPatient(patientId, { take: limit + 1, orderBy: created_at DESC })
477
+ - ... (other sources)
478
+ 3. Transform each result set into TimelineEntry[]
479
+ 4. Merge all entries into a single array
480
+ 5. Sort by timestamp DESC
481
+ 6. Apply cursor-based or offset pagination (take first "limit" entries)
482
+ 7. Return { entries, hasMore, nextCursor }
483
+ ```
484
+
485
+ **Why not a materialized view or dedicated table:**
486
+
487
+ | Approach | Pros | Cons |
488
+ |----------|------|------|
489
+ | Materialized view | Fast reads | Stale data, complex refresh triggers, PostgreSQL-specific |
490
+ | Dedicated timeline_events table | Simple queries | Write amplification (every action writes twice), schema coupling |
491
+ | **Parallel queries + in-memory merge** | Always fresh, no write overhead, source-independent | Slightly more complex service code, N+1 risk (mitigated by per-source LIMIT) |
492
+
493
+ The parallel query approach is chosen because:
494
+ - Timeline is read infrequently (only on patient detail page).
495
+ - Each source query is indexed and fast (all have `(patient_id, timestamp DESC)` indexes).
496
+ - Over-fetching per source (limit + 1) and merging gives correct pagination without full table scans.
497
+
498
+ ### 6.4 Endpoint
499
+
500
+ | Method | Path | Permission | Description |
501
+ |--------|------|-----------|-------------|
502
+ | GET | `/patients/:id/timeline` | `patients:read` | Unified activity timeline |
503
+
504
+ **Query parameters:**
505
+
506
+ | Parameter | Type | Default | Description |
507
+ |-----------|------|---------|-------------|
508
+ | `page` | Int | `1` | Page number (offset-based) |
509
+ | `limit` | Int | `20` | Items per page (max 50) |
510
+ | `types` | String | all | Comma-separated filter: `appointment,call,session,operator_request,treatment_plan,document,note,photo_set,task` |
511
+
512
+ ---
513
+
514
+ ## 7. Auto-Creation from AI Contact
515
+
516
+ When the AI system receives a message or call from an unknown phone number, a Patient record is created automatically. This replaces the current `WhatsAppContactProfile` upsert logic.
517
+
518
+ ### 7.1 Sequence Diagram
519
+
520
+ ```
521
+ Incoming WhatsApp message from unknown phone
522
+ |
523
+ v
524
+ Webhook handler extracts sender phone (E.164)
525
+ |
526
+ v
527
+ PatientService.findOrCreate(tenantId, phone, metadata)
528
+ |
529
+ v
530
+ SELECT FROM patients WHERE patient_tenant_id = :tenantId AND patient_phone = :phone
531
+ |
532
+ +-- Found --> return existing Patient
533
+ |
534
+ +-- Not found
535
+ |
536
+ v
537
+ 1. Determine clinic from routing context (PhoneNumber -> Clinic)
538
+ 2. Extract display name from WhatsApp push name or voice call caller ID
539
+ 3. Attempt to split display name into first + last name
540
+ (heuristic: last word = last name, rest = first name)
541
+ 4. INSERT Patient:
542
+ patient_phone = phone
543
+ patient_first_name = extracted or "Bilinmiyor"
544
+ patient_last_name = extracted or "Bilinmiyor"
545
+ patient_display_name = raw push name or phone number
546
+ patient_clinic_id = routed clinic ID (nullable)
547
+ patient_source = channel ('whatsapp' | 'voice_call' | 'instagram')
548
+ patient_first_contact_at = now()
549
+ patient_last_contact_at = now()
550
+ 5. INSERT PatientMedicalHistory (empty, linked to new patient)
551
+ 6. Link WhatsAppChat.chat_patient_id = new patient ID
552
+ 7. Return new Patient
553
+ ```
554
+
555
+ ### 7.2 Channel-Specific Behavior
556
+
557
+ | Channel | Display Name Source | Source Value | Additional Data |
558
+ |---------|-------------------|--------------|-----------------|
559
+ | WhatsApp (Baileys) | Push name from `message.pushName` | `whatsapp` | -- |
560
+ | WhatsApp (Meta Business) | Profile name from contact info | `whatsapp` | -- |
561
+ | Voice Call (Inbound) | Caller ID (if available) | `voice_call` | -- |
562
+ | Voice Call (Outbound) | From campaign/validation entry data | `campaign` | `patient_source_detail` = campaign name |
563
+ | Instagram DM | Instagram username | `instagram_ad` | `patient_source_detail` = IG handle |
564
+
565
+ ### 7.3 Deduplication
566
+
567
+ The `(patient_tenant_id, patient_phone)` unique constraint prevents duplicates. If two concurrent requests try to create the same patient, the second INSERT fails with a unique constraint violation. The service catches this, re-reads the existing record, and returns it.
568
+
569
+ ```typescript
570
+ async findOrCreate(tenantId: string, phone: string, metadata: PatientAutoCreateMetadata): Promise<Patient> {
571
+ // Try to find existing
572
+ const existing = await this.prisma.patient.findUnique({
573
+ where: { patient_tenant_id_patient_phone: { patient_tenant_id: tenantId, patient_phone: phone } },
574
+ });
575
+ if (existing) {
576
+ // Update last_contact_at
577
+ await this.prisma.patient.update({
578
+ where: { patient_id: existing.patient_id },
579
+ data: { patient_last_contact_at: new Date() },
580
+ });
581
+ return existing;
582
+ }
583
+
584
+ // Create new -- handle race condition
585
+ try {
586
+ const patient = await this.prisma.$transaction(async (tx) => {
587
+ const p = await tx.patient.create({ data: { ... } });
588
+ await tx.patientMedicalHistory.create({ data: { history_patient_id: p.patient_id } });
589
+ return p;
590
+ });
591
+ return patient;
592
+ } catch (err) {
593
+ if (err.code === 'P2002') { // Prisma unique constraint violation
594
+ return this.prisma.patient.findUniqueOrThrow({
595
+ where: { patient_tenant_id_patient_phone: { patient_tenant_id: tenantId, patient_phone: phone } },
596
+ });
597
+ }
598
+ throw err;
599
+ }
600
+ }
601
+ ```
602
+
603
+ ---
604
+
605
+ ## 8. API Endpoints
606
+
607
+ ### Patient CRUD
608
+
609
+ | Method | Path | Permission | Description |
610
+ |--------|------|-----------|-------------|
611
+ | GET | `/patients` | `patients:read` | List patients (paginated, searchable by name/phone, filterable by clinic/tag/source) |
612
+ | POST | `/patients` | `patients:create` | Create patient manually (walk-in, phone registration) |
613
+ | GET | `/patients/:id` | `patients:read` | Patient detail with medical history and stats |
614
+ | PUT | `/patients/:id` | `patients:update` | Update demographics, tags |
615
+ | DELETE | `/patients/:id` | `patients:delete` | Soft-delete (set `patient_is_active = false`) |
616
+
617
+ ### Search
618
+
619
+ | Method | Path | Permission | Description |
620
+ |--------|------|-----------|-------------|
621
+ | GET | `/patients/search` | `patients:read` | Quick search by name or phone (autocomplete). Returns max 10 results. Used by search bars across the dashboard |
622
+
623
+ **Search query logic:** `WHERE (patient_first_name ILIKE :q% OR patient_last_name ILIKE :q% OR patient_phone LIKE :q%) AND patient_tenant_id = :tenantId AND patient_is_active = true`. Uses the `idx_patients_tenant_name` index for name searches and the unique index for phone searches.
624
+
625
+ ### Timeline
626
+
627
+ | Method | Path | Permission | Description |
628
+ |--------|------|-----------|-------------|
629
+ | GET | `/patients/:id/timeline` | `patients:read` | Unified activity timeline (see [Section 6](#6-patient-timeline)) |
630
+
631
+ ### Medical History
632
+
633
+ | Method | Path | Permission | Description |
634
+ |--------|------|-----------|-------------|
635
+ | GET | `/patients/:id/medical-history` | `patients:read` | Medical history / anamnesis |
636
+ | PUT | `/patients/:id/medical-history` | `patients:update` | Update medical history. Sets `history_anamnesis_completed = true` and `history_anamnesis_date = now()` on first complete submission |
637
+
638
+ ### Documents
639
+
640
+ | Method | Path | Permission | Description |
641
+ |--------|------|-----------|-------------|
642
+ | GET | `/patients/:id/documents` | `patients:read` | List documents (with signed URLs) |
643
+ | POST | `/patients/:id/documents` | `patients:update` | Upload document (multipart/form-data -> MinIO) |
644
+ | DELETE | `/patients/:id/documents/:docId` | `patients:update` | Delete document (removes from MinIO and DB) |
645
+
646
+ ### Notes
647
+
648
+ | Method | Path | Permission | Description |
649
+ |--------|------|-----------|-------------|
650
+ | GET | `/patients/:id/notes` | `patients:read` | List notes (pinned first, then by created_at DESC) |
651
+ | POST | `/patients/:id/notes` | `patients:update` | Add note (author = current user) |
652
+ | PUT | `/patients/:id/notes/:noteId` | `patients:update` | Edit note text or pin/unpin |
653
+ | DELETE | `/patients/:id/notes/:noteId` | `patients:update` | Delete note |
654
+
655
+ ### Merge
656
+
657
+ | Method | Path | Permission | Description |
658
+ |--------|------|-----------|-------------|
659
+ | POST | `/patients/merge` | `patients:update` | Merge duplicate patients (see [Section 10](#10-patient-merge-duplicate-resolution)) |
660
+
661
+ ### List Endpoint Query Parameters
662
+
663
+ `GET /patients` supports the following query parameters:
664
+
665
+ | Parameter | Type | Default | Description |
666
+ |-----------|------|---------|-------------|
667
+ | `page` | Int | `1` | Page number |
668
+ | `limit` | Int | `20` | Items per page (max 100) |
669
+ | `search` | String | -- | Search by name or phone (ILIKE) |
670
+ | `clinic_id` | UUID | -- | Filter by primary clinic |
671
+ | `source` | String | -- | Filter by acquisition source |
672
+ | `tag` | String | -- | Filter by tag (array containment) |
673
+ | `is_active` | Boolean | `true` | Include archived patients |
674
+ | `sort` | String | `last_contact_at` | Sort field: `last_contact_at`, `first_name`, `created_at`, `total_appointments` |
675
+ | `order` | String | `desc` | Sort order: `asc`, `desc` |
676
+
677
+ ---
678
+
679
+ ## 9. Permissions
680
+
681
+ ### New Permission Keys
682
+
683
+ | Permission | Description |
684
+ |------------|-------------|
685
+ | `patients:read` | View patient list and detail pages |
686
+ | `patients:create` | Create new patients manually (walk-in, phone registration) |
687
+ | `patients:update` | Edit patient demographics, medical history, documents, notes |
688
+ | `patients:delete` | Soft-delete (archive) patients |
689
+ | `patients:export` | Export patient data (CSV download, KVKK data packages) |
690
+
691
+ ### Role Mapping
692
+
693
+ | Role | `patients:read` | `patients:create` | `patients:update` | `patients:delete` | `patients:export` |
694
+ |------|-----------------|--------------------|--------------------|--------------------|--------------------|
695
+ | owner | Yes | Yes | Yes | Yes | Yes |
696
+ | admin | Yes | Yes | Yes | No | Yes |
697
+ | doctor | Yes | No | Yes (medical history, notes only) | No | No |
698
+ | receptionist | Yes | Yes | Yes (demographics only, not medical history) | No | No |
699
+
700
+ ### Role-Specific Update Restrictions
701
+
702
+ The `patients:update` permission is further scoped by role at the service level:
703
+
704
+ - **doctor:** Can update `PatientMedicalHistory` and `PatientNote`. Cannot update patient demographics (`patient_first_name`, `patient_last_name`, `patient_tags`, etc.).
705
+ - **receptionist:** Can update patient demographics and add notes. Cannot update `PatientMedicalHistory`.
706
+ - **admin/owner:** Can update everything.
707
+
708
+ This is enforced in `PatientsService.update()` and `MedicalHistoryService.update()` by checking the user's role before applying changes. Not a separate permission key -- role-based field filtering.
709
+
710
+ ---
711
+
712
+ ## 10. Patient Merge (Duplicate Resolution)
713
+
714
+ Duplicates arise when the same person contacts via different channels or phone numbers. For example: a patient texts from their personal WhatsApp, then calls from their work phone. Two Patient records exist for the same person.
715
+
716
+ ### 10.1 Merge Endpoint
717
+
718
+ ```
719
+ POST /patients/merge
720
+ {
721
+ "primary_patient_id": "uuid-1", // Keeps its ID, receives all data
722
+ "secondary_patient_id": "uuid-2" // Merges into primary, gets soft-deleted
723
+ }
724
+ ```
725
+
726
+ Permission: `patients:update`
727
+
728
+ ### 10.2 Merge Algorithm
729
+
730
+ ```
731
+ POST /patients/merge { primary_patient_id, secondary_patient_id }
732
+ |
733
+ v
734
+ 1. Validate both patients belong to the same tenant
735
+ 2. Validate neither patient is already soft-deleted
736
+ 3. Begin transaction:
737
+ |
738
+ +-- 4. Re-link all child records from secondary to primary:
739
+ | UPDATE appointments SET appt_patient_id = :primary WHERE appt_patient_id = :secondary
740
+ | UPDATE whatsapp_chats SET chat_patient_id = :primary WHERE chat_patient_id = :secondary
741
+ | UPDATE patient_documents SET doc_patient_id = :primary WHERE doc_patient_id = :secondary
742
+ | UPDATE patient_notes SET note_patient_id = :primary WHERE note_patient_id = :secondary
743
+ | UPDATE patient_photo_sets SET ... WHERE ... = :secondary
744
+ | UPDATE tasks SET ... WHERE ... = :secondary
745
+ | (treatment_plans, operator_requests follow same pattern)
746
+ |
747
+ +-- 5. Merge medical history:
748
+ | Primary wins on scalar conflicts (smoking, alcohol, pregnancy_status)
749
+ | Arrays are unioned (allergies, conditions, medications)
750
+ | Notes are concatenated with separator
751
+ | If secondary has anamnesis_completed=true and primary does not, adopt secondary's date
752
+ |
753
+ +-- 6. Merge patient fields:
754
+ | patient_tags = UNION of both tag arrays
755
+ | patient_total_sessions = primary + secondary
756
+ | patient_total_appointments = primary + secondary
757
+ | patient_first_contact_at = MIN(primary, secondary)
758
+ | patient_last_contact_at = MAX(primary, secondary)
759
+ | patient_ai_summary = primary (regenerated async after merge)
760
+ | Demographics: primary wins (first_name, last_name, email, date_of_birth, etc.)
761
+ |
762
+ +-- 7. Store secondary phone as alternate contact:
763
+ | Create PatientNote on primary: "Birlestirildi: eski telefon {secondary.phone}"
764
+ |
765
+ +-- 8. Soft-delete secondary patient:
766
+ | UPDATE patients SET patient_is_active = false WHERE patient_id = :secondary
767
+ |
768
+ +-- 9. Create audit log entry:
769
+ | action = 'patient.merge'
770
+ | details = { primary_id, secondary_id, secondary_phone, records_moved: { ... } }
771
+ |
772
+ v
773
+ Commit transaction
774
+ |
775
+ v
776
+ 10. Queue async job: regenerate AI summary for primary patient
777
+ |
778
+ v
779
+ Return merged primary patient
780
+ ```
781
+
782
+ ### 10.3 Edge Cases
783
+
784
+ | Scenario | Handling |
785
+ |----------|----------|
786
+ | Both patients have medical history with `anamnesis_completed = true` | Primary's anamnesis wins. Secondary's unique data (extra allergies, conditions) is merged in. Note added: "Anamnez birlestirildi" |
787
+ | Circular merge (A -> B, then B -> A) | Rejected -- secondary must be `is_active = true` |
788
+ | Self-merge (same ID for both) | Rejected at validation |
789
+ | Different tenants | Rejected at validation |
790
+ | Secondary has ongoing WhatsApp session | Sessions are re-linked. No active session disruption -- the session continues under the primary patient |
791
+
792
+ ---
793
+
794
+ ## 11. WhatsAppContactProfile Migration
795
+
796
+ The `whatsapp_contact_profiles` table is **removed** in the new system. All its data lives in `patients` and related tables.
797
+
798
+ ### 11.1 Field Mapping
799
+
800
+ | Old Field (ContactProfile) | New Location | Notes |
801
+ |---------------------------|-------------|-------|
802
+ | `contact_phone` | `patient_phone` | Direct mapping |
803
+ | `contact_display_name` | `patient_display_name` | Direct mapping |
804
+ | `contact_notes` | `PatientNote` (migrated as first note) | Created as a note with `note_author_id` = system user, `note_text` = contact_notes value |
805
+ | `contact_tags` | `patient_tags` | Direct mapping |
806
+ | `contact_ai_summary` | `patient_ai_summary` | Direct mapping |
807
+ | `contact_preferred_language` | `patient_preferred_language` | Direct mapping. Empty string becomes `"tr"` |
808
+ | `contact_metadata` | Dropped | Was unused JSONB. Any non-empty values are logged before deletion |
809
+ | `contact_first_seen_at` | `patient_first_contact_at` | Direct mapping |
810
+ | `contact_last_seen_at` | `patient_last_contact_at` | Direct mapping |
811
+ | `contact_total_sessions` | `patient_total_sessions` | Direct mapping |
812
+ | `contact_total_messages` | Dropped | Computed from `whatsapp_messages` table at query time. No need to store |
813
+
814
+ ### 11.2 FK Migration
815
+
816
+ `WhatsAppChat.chat_contact_id` is replaced by `WhatsAppChat.chat_patient_id`:
817
+
818
+ ```
819
+ Migration steps:
820
+ 1. Add column: whatsapp_chats.chat_patient_id (UUID, nullable)
821
+ 2. For each chat with chat_contact_id:
822
+ a. Look up contact_profile by chat_contact_id
823
+ b. Find or create Patient by (tenant_id, contact_phone)
824
+ c. Set chat_patient_id = patient_id
825
+ 3. Drop column: whatsapp_chats.chat_contact_id
826
+ 4. Drop table: whatsapp_contact_profiles
827
+ ```
828
+
829
+ ### 11.3 Migration Script (Prisma Migration)
830
+
831
+ ```sql
832
+ -- Step 1: Create patients from contact profiles
833
+ INSERT INTO patients (
834
+ patient_id, patient_tenant_id, patient_phone, patient_first_name, patient_last_name,
835
+ patient_display_name, patient_tags, patient_ai_summary, patient_preferred_language,
836
+ patient_first_contact_at, patient_last_contact_at, patient_total_sessions,
837
+ patient_created_at, patient_updated_at
838
+ )
839
+ SELECT
840
+ gen_random_uuid(),
841
+ contact_tenant_id,
842
+ contact_phone,
843
+ CASE
844
+ WHEN contact_display_name = '' OR contact_display_name = 'Bilinmiyor' THEN 'Bilinmiyor'
845
+ ELSE split_part(contact_display_name, ' ', 1)
846
+ END,
847
+ CASE
848
+ WHEN contact_display_name = '' OR contact_display_name = 'Bilinmiyor' THEN 'Bilinmiyor'
849
+ WHEN array_length(string_to_array(contact_display_name, ' '), 1) > 1
850
+ THEN substring(contact_display_name from position(' ' in contact_display_name) + 1)
851
+ ELSE 'Bilinmiyor'
852
+ END,
853
+ COALESCE(NULLIF(contact_display_name, ''), contact_phone),
854
+ contact_tags,
855
+ contact_ai_summary,
856
+ COALESCE(NULLIF(contact_preferred_language, ''), 'tr'),
857
+ contact_first_seen_at,
858
+ contact_last_seen_at,
859
+ contact_total_sessions,
860
+ NOW(),
861
+ NOW()
862
+ FROM whatsapp_contact_profiles
863
+ ON CONFLICT (patient_tenant_id, patient_phone) DO NOTHING;
864
+
865
+ -- Step 2: Create empty medical histories for migrated patients
866
+ INSERT INTO patient_medical_histories (history_id, history_patient_id, history_created_at, history_updated_at)
867
+ SELECT gen_random_uuid(), patient_id, NOW(), NOW()
868
+ FROM patients
869
+ WHERE NOT EXISTS (
870
+ SELECT 1 FROM patient_medical_histories WHERE history_patient_id = patients.patient_id
871
+ );
872
+
873
+ -- Step 3: Migrate contact_notes to patient_notes (where non-empty)
874
+ INSERT INTO patient_notes (note_id, note_patient_id, note_tenant_id, note_author_id, note_text, note_created_at, note_updated_at)
875
+ SELECT
876
+ gen_random_uuid(),
877
+ p.patient_id,
878
+ p.patient_tenant_id,
879
+ (SELECT user_id FROM users WHERE user_tenant_id = p.patient_tenant_id AND user_role = 'owner' LIMIT 1),
880
+ cp.contact_notes,
881
+ cp.contact_created_at,
882
+ NOW()
883
+ FROM whatsapp_contact_profiles cp
884
+ JOIN patients p ON p.patient_tenant_id = cp.contact_tenant_id AND p.patient_phone = cp.contact_phone
885
+ WHERE cp.contact_notes IS NOT NULL AND cp.contact_notes != '';
886
+
887
+ -- Step 4: Link chats to patients
888
+ UPDATE whatsapp_chats wc
889
+ SET chat_patient_id = p.patient_id
890
+ FROM whatsapp_contact_profiles cp
891
+ JOIN patients p ON p.patient_tenant_id = cp.contact_tenant_id AND p.patient_phone = cp.contact_phone
892
+ WHERE wc.chat_contact_id = cp.contact_id;
893
+
894
+ -- Step 5: Drop old column and table (after verification)
895
+ ALTER TABLE whatsapp_chats DROP COLUMN chat_contact_id;
896
+ DROP TABLE whatsapp_contact_profiles;
897
+ ```
898
+
899
+ ---
900
+
901
+ ## 12. NestJS Module Structure
902
+
903
+ ```
904
+ src/patients/
905
+ patients.module.ts # PatientsModule -- imports PrismaModule, MinioModule, UsersModule
906
+ patients.controller.ts # /patients/* endpoints (CRUD, search, merge, timeline)
907
+ patients.service.ts # Patient CRUD, search, merge logic, auto-create
908
+ medical-history.service.ts # Anamnesis CRUD (1:1 with patient)
909
+ documents.service.ts # Upload/delete with MinIO, signed URL generation
910
+ notes.service.ts # Note CRUD (create, edit, pin, delete)
911
+ timeline.service.ts # Multi-source timeline aggregation (parallel queries + merge)
912
+ dto/
913
+ create-patient.dto.ts # Validation: phone (E.164), first_name, last_name required
914
+ update-patient.dto.ts # Partial update (PartialType)
915
+ update-medical-history.dto.ts # All fields optional
916
+ create-note.dto.ts # note_text required
917
+ upload-document.dto.ts # doc_type, doc_name required (file via multipart)
918
+ merge-patients.dto.ts # primary_patient_id, secondary_patient_id required
919
+ patient-query.dto.ts # Pagination, search, filter params
920
+ timeline-query.dto.ts # Pagination, type filter
921
+ ```
922
+
923
+ **Module dependencies:**
924
+
925
+ | Import | Reason |
926
+ |--------|--------|
927
+ | `PrismaModule` | Database access |
928
+ | `MinioModule` | Document upload/download/delete |
929
+ | `UsersModule` | Resolve note authors, validate uploaded_by |
930
+ | `AuditModule` | Log merge operations and sensitive changes |
931
+ | `BullModule` | Queue AI summary regeneration jobs |
932
+
933
+ **Exports:**
934
+
935
+ `PatientsService` is exported for use by other modules:
936
+ - `WhatsAppModule` calls `patientsService.findOrCreate()` on incoming messages.
937
+ - `CallsModule` calls `patientsService.findOrCreate()` on inbound calls.
938
+ - `AppointmentsModule` calls `patientsService.incrementAppointmentCount()` on booking.
939
+
940
+ ---
941
+
942
+ ## 13. Feature Flag
943
+
944
+ No feature flag. The patients module is core functionality, always enabled for all tenants. It replaces `WhatsAppContactProfile` which was also always-on.
945
+
946
+ ---
947
+
948
+ ## 14. Affected Existing Models
949
+
950
+ The Patient entity becomes the FK target for several existing models. These models gain a `patient_id` foreign key column.
951
+
952
+ ### 14.1 Models with Direct FK
953
+
954
+ | Model | New Column | Replaces | Notes |
955
+ |-------|-----------|----------|-------|
956
+ | `WhatsAppChat` | `chat_patient_id` (UUID, FK) | `chat_contact_id` | Required migration. See [Section 11](#11-whatsappcontactprofile-migration) |
957
+ | `Appointment` | `appt_patient_id` (UUID, FK) | Phone-based matching | Replaces `appt_patient_phone` as the join key. `appt_patient_phone` is kept for display but is no longer the primary link |
958
+ | `Lead` | `lead_patient_id` (UUID, nullable FK) | -- | Optional. Linked when a patient record exists for the lead's phone. Null for leads that have not yet become patients |
959
+
960
+ ### 14.2 Models with Indirect Patient Resolution
961
+
962
+ | Model | Resolution Path | Notes |
963
+ |-------|----------------|-------|
964
+ | `OperatorRequest` | `opreq_session_id` -> `WhatsAppSession` -> `WhatsAppChat.chat_patient_id` | No direct FK needed. Resolved via JOINs |
965
+ | `Call` | `call_caller_phone` matched against `patient_phone` at query time | No FK. Phone matching is done in the timeline query. Adding a FK would require resolving patient on every inbound call before the call connects (too slow) |
966
+ | `WhatsAppSession` | `session_chat_id` -> `WhatsAppChat.chat_patient_id` | No direct FK needed. Resolved via one JOIN through chat |
967
+ | `WhatsAppMessage` | `msg_chat_id` -> `WhatsAppChat.chat_patient_id` | No direct FK needed. Messages are always accessed in chat context |
968
+
969
+ ### 14.3 Appointment Model Changes
970
+
971
+ ```prisma
972
+ // Before (current):
973
+ model Appointment {
974
+ appt_patient_name String // Free text name
975
+ appt_patient_phone String // E.164 phone -- the only link to patient
976
+ // ...
977
+ }
978
+
979
+ // After (with patients):
980
+ model Appointment {
981
+ appt_patient_id String // FK to patients (required for new appointments)
982
+ appt_patient_name String // Kept for display (denormalized)
983
+ appt_patient_phone String // Kept for Google Calendar events and SMS
984
+ // ...
985
+
986
+ patient Patient @relation(fields: [appt_patient_id], references: [patient_id])
987
+ }
988
+ ```
989
+
990
+ `appt_patient_name` and `appt_patient_phone` are kept as denormalized fields because:
991
+ - Google Calendar event creation needs the name and phone as plain strings.
992
+ - SMS reminders need the phone number without a JOIN.
993
+ - Historical appointments (pre-migration) may not have a `patient_id` -- the denormalized fields ensure they remain displayable.
994
+
995
+ ### 14.4 WhatsAppChat Model Changes
996
+
997
+ ```prisma
998
+ // Before (current):
999
+ model WhatsAppChat {
1000
+ chat_contact_id String? // FK to whatsapp_contact_profiles
1001
+ // ...
1002
+ }
1003
+
1004
+ // After (with patients):
1005
+ model WhatsAppChat {
1006
+ chat_patient_id String? // FK to patients
1007
+ // ...
1008
+
1009
+ patient Patient? @relation(fields: [chat_patient_id], references: [patient_id])
1010
+ }
1011
+ ```
1012
+
1013
+ `chat_patient_id` is nullable to handle edge cases where a chat exists but the patient record has not yet been created (should not happen in normal flow, but defensive).
1014
+
1015
+ ### 14.5 Lead Model Changes
1016
+
1017
+ ```prisma
1018
+ // Before (current):
1019
+ model Lead {
1020
+ lead_phone String // E.164 phone
1021
+ // ...
1022
+ }
1023
+
1024
+ // After (with patients):
1025
+ model Lead {
1026
+ lead_patient_id String? // FK to patients (nullable)
1027
+ lead_phone String // Kept -- leads may exist before patient creation
1028
+ // ...
1029
+
1030
+ patient Patient? @relation(fields: [lead_patient_id], references: [patient_id])
1031
+ }
1032
+ ```
1033
+
1034
+ `lead_patient_id` is nullable because leads can exist before a patient record is created (e.g., a lead from a campaign that has not yet contacted the clinic). When the lead's phone matches an existing patient, the link is established.