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,2506 @@
1
+ import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { InjectQueue } from '@nestjs/bullmq';
4
+ import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
5
+ import type { Queue } from 'bullmq';
6
+ import WebSocket from 'ws';
7
+ import type Redis from 'ioredis';
8
+ import { PrismaService } from '../database/prisma.service.js';
9
+ import { EncryptionService } from '../integrations/encryption/encryption.service.js';
10
+ import { ElevenLabsService } from '../integrations/elevenlabs/elevenlabs.service.js';
11
+ import { MetaWhatsAppService } from '../integrations/meta-business/meta-whatsapp.service.js';
12
+ import { MetaInstagramService } from '../integrations/meta-business/meta-instagram.service.js';
13
+ import { MinioService } from '../integrations/minio/minio.service.js';
14
+ import { ElevenLabsWsAdapter } from '../tools/adapters/elevenlabs-ws.adapter.js';
15
+ import { REDIS_CLIENT } from '../redis/redis.module.js';
16
+ import type { ToolContext } from '../tools/tool.interfaces.js';
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Types */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ export interface IncomingMessagePayload {
23
+ /** Tenant ID resolved from webhook/phone number */
24
+ tenantId: string;
25
+ /** Clinic ID resolved from phone number mapping */
26
+ clinicId: string;
27
+ /** WhatsApp or Instagram external chat ID */
28
+ externalChatId: string;
29
+ /** Channel: whatsapp | instagram */
30
+ channel: 'whatsapp' | 'instagram';
31
+ /** Phone number ID (Meta Business) */
32
+ phoneNumberId?: string;
33
+ /** External account ID (for isSender echo detection) */
34
+ externalAccountId?: string;
35
+ /** Whether this message was sent by our own account */
36
+ isSender?: boolean;
37
+ /** Sender info */
38
+ sender: {
39
+ phone: string;
40
+ name?: string;
41
+ profilePicUrl?: string;
42
+ };
43
+ /** Message content */
44
+ message: {
45
+ externalId: string;
46
+ text: string;
47
+ mediaType?: string;
48
+ mediaKey?: string;
49
+ mediaMime?: string;
50
+ mediaFilename?: string;
51
+ /** Content type from the platform (e.g. 'audioMessage', 'audio') */
52
+ contentType?: string;
53
+ quotedText?: string;
54
+ adContext?: Record<string, any>;
55
+ /** Structured metadata for non-text messages (location, contacts, reaction, button reply, etc.) */
56
+ metadata?: Record<string, any>;
57
+ };
58
+ /** Timestamp from the platform */
59
+ timestamp: Date;
60
+ }
61
+
62
+ /** Resolved agent with all config needed for any AI provider */
63
+ interface ResolvedAgent {
64
+ agent_id: string;
65
+ agent_elevenlabs_id: string;
66
+ agent_name: string;
67
+ agent_system_prompt: string;
68
+ agent_ai_provider: string | null;
69
+ agent_ai_model: string | null;
70
+ agent_language: string;
71
+ agent_greeting: string;
72
+ agent_temperature: number;
73
+ agent_max_tokens: number;
74
+ agent_enabled_tools: string[];
75
+ agent_channels: string[];
76
+ agent_extra_config: any;
77
+ }
78
+
79
+ /** In-memory ElevenLabs WS connection state */
80
+ interface WsConnection {
81
+ ws: WebSocket;
82
+ agentId: string;
83
+ tenantId: string;
84
+ sessionId: string;
85
+ chatId: string;
86
+ conversationId?: string;
87
+ isReady: boolean;
88
+ readyPromise: Promise<void>;
89
+ readyResolve: () => void;
90
+ }
91
+
92
+ /* ------------------------------------------------------------------ */
93
+ /* System message patterns — Turkish + English */
94
+ /* ------------------------------------------------------------------ */
95
+
96
+ const SYSTEM_MESSAGE_PATTERNS: RegExp[] = [
97
+ /sesli arama/i,
98
+ /görüntülü arama/i,
99
+ /voice call/i,
100
+ /video call/i,
101
+ /Bu mesaj silindi/i,
102
+ /This message was deleted/i,
103
+ /kaybolan mesaj/i,
104
+ /disappearing message/i,
105
+ /uçtan uca şifrel/i,
106
+ /end-to-end encrypt/i,
107
+ /canlı konum/i,
108
+ /live location/i,
109
+ /ödeme/i,
110
+ /grup.*(oluştur|kurdu|değiştir|ekledi|çıkardı)/i,
111
+ /bir kez görüntüle/i,
112
+ /view once/i,
113
+ /WhatsApp.*(güncelle|update)/i,
114
+ /missed.*call/i,
115
+ /cevapsız.*arama/i,
116
+ /güvenlik kodu değişti/i,
117
+ /security code changed/i,
118
+ /numara değişti/i,
119
+ /changed.*(number|phone)/i,
120
+ /kişiyi engelle/i,
121
+ /blocked this contact/i,
122
+ ];
123
+
124
+ /**
125
+ * The main AI chat engine.
126
+ * Manages WhatsApp/Instagram chat sessions, ElevenLabs AI WebSocket
127
+ * connections, grace periods, and message buffering.
128
+ */
129
+ @Injectable()
130
+ export class WhatsAppAgentService implements OnModuleDestroy {
131
+ private readonly logger = new Logger(WhatsAppAgentService.name);
132
+ private readonly messageEncryptionKey: string;
133
+ private readonly tokenEncryptionKey: string;
134
+ private readonly patientSummaryKey: string;
135
+
136
+ /** In-memory WebSocket connections keyed by chatId */
137
+ private readonly wsConnections = new Map<string, WsConnection>();
138
+
139
+ constructor(
140
+ private readonly prisma: PrismaService,
141
+ private readonly encryption: EncryptionService,
142
+ private readonly config: ConfigService,
143
+ private readonly eventEmitter: EventEmitter2,
144
+ private readonly elevenlabsService: ElevenLabsService,
145
+ private readonly metaWhatsApp: MetaWhatsAppService,
146
+ private readonly metaInstagram: MetaInstagramService,
147
+ private readonly minio: MinioService,
148
+ private readonly wsAdapter: ElevenLabsWsAdapter,
149
+ @Inject(REDIS_CLIENT) private readonly redis: Redis,
150
+ @InjectQueue('whatsapp-grace')
151
+ private readonly graceQueue: Queue,
152
+ @InjectQueue('whatsapp-message-buffer')
153
+ private readonly messageBufferQueue: Queue,
154
+ @InjectQueue('whatsapp-session-cleanup')
155
+ private readonly sessionCleanupQueue: Queue,
156
+ @InjectQueue('whatsapp-profile-update')
157
+ private readonly profileUpdateQueue: Queue,
158
+ @InjectQueue('whatsapp-session-labels')
159
+ private readonly sessionLabelsQueue: Queue,
160
+ ) {
161
+ this.messageEncryptionKey = this.config.getOrThrow<string>(
162
+ 'MESSAGE_ENCRYPTION_KEY',
163
+ );
164
+ this.tokenEncryptionKey = this.config.get<string>(
165
+ 'META_TOKEN_ENCRYPTION_KEY',
166
+ '',
167
+ );
168
+ this.patientSummaryKey = this.config.get<string>(
169
+ 'PATIENT_SUMMARY_ENCRYPTION_KEY',
170
+ '',
171
+ );
172
+ }
173
+
174
+ onModuleDestroy() {
175
+ // Close all open WebSocket connections on shutdown
176
+ for (const [chatId] of this.wsConnections) {
177
+ this.closeConnection(chatId);
178
+ }
179
+ }
180
+
181
+ /* ================================================================== */
182
+ /* INCOMING MESSAGE — main entry point from webhooks */
183
+ /* ================================================================== */
184
+
185
+ async handleIncomingMessage(payload: IncomingMessagePayload): Promise<void> {
186
+ const {
187
+ tenantId,
188
+ clinicId,
189
+ externalChatId,
190
+ channel,
191
+ sender,
192
+ message,
193
+ } = payload;
194
+
195
+ this.logger.debug(
196
+ `Incoming ${channel} message from ${sender.phone} in chat ${externalChatId}`,
197
+ );
198
+
199
+ // ── 1. System message filtering ──────────────────────────────────
200
+ if (this.isSystemMessage(message.text)) {
201
+ this.logger.debug(
202
+ `System message detected, storing without session trigger: "${message.text.substring(0, 60)}"`,
203
+ );
204
+ // Store the message for audit trail but do not trigger a session
205
+ await this.storeSystemMessage(tenantId, externalChatId, message, sender);
206
+ return;
207
+ }
208
+
209
+ // ── 2. Echo deduplication ────────────────────────────────────────
210
+ if (payload.isSender) {
211
+ const echoHandled = await this.handleEchoMessage(
212
+ tenantId,
213
+ externalChatId,
214
+ message.externalId,
215
+ );
216
+ if (echoHandled) return;
217
+ }
218
+
219
+ // ── 3. Resolve phone number → clinic ─────────────────────────────
220
+ const clinic = await this.prisma.clinic.findUnique({
221
+ where: { clinic_id: clinicId },
222
+ select: {
223
+ clinic_id: true,
224
+ clinic_tenant_id: true,
225
+ clinic_blacklisted_numbers: true,
226
+ clinic_grace_period_seconds: true,
227
+ clinic_fixed_first_message: true,
228
+ clinic_language: true,
229
+ },
230
+ });
231
+
232
+ if (!clinic) {
233
+ this.logger.warn(`Clinic ${clinicId} not found, dropping message`);
234
+ return;
235
+ }
236
+
237
+ // ── 3b. Voice transcription (STT) ───────────────────────────────
238
+ const contentType = message.contentType || message.mediaType;
239
+ if (
240
+ contentType &&
241
+ (contentType === 'audioMessage' || contentType === 'audio') &&
242
+ message.mediaKey
243
+ ) {
244
+ const sttEnabled = await this.isTenantFeatureEnabled(
245
+ tenantId,
246
+ 'voice_transcription',
247
+ );
248
+
249
+ if (sttEnabled) {
250
+ try {
251
+ // Download audio from Meta CDN
252
+ const metaConn = await this.prisma.clinicMetaConnection.findFirst({
253
+ where: {
254
+ meta_clinic_id: clinicId,
255
+ meta_type: 'whatsapp',
256
+ meta_is_active: true,
257
+ },
258
+ select: {
259
+ meta_encrypted_access_token: true,
260
+ },
261
+ });
262
+
263
+ if (metaConn?.meta_encrypted_access_token) {
264
+ const accessToken = this.tokenEncryptionKey
265
+ ? this.encryption.decrypt(
266
+ metaConn.meta_encrypted_access_token,
267
+ this.tokenEncryptionKey,
268
+ )
269
+ : metaConn.meta_encrypted_access_token;
270
+
271
+ const audioBuffer = await this.metaWhatsApp.downloadMedia(
272
+ message.mediaKey,
273
+ accessToken,
274
+ );
275
+
276
+ const sttResult = await this.elevenlabsService.transcribe(
277
+ audioBuffer,
278
+ clinic.clinic_language || 'tr',
279
+ );
280
+
281
+ if (sttResult.text?.trim()) {
282
+ message.text = sttResult.text;
283
+ message.contentType = 'transcribedAudio';
284
+ message.mediaType = 'transcribedAudio';
285
+
286
+ // Log STT usage (fire-and-forget)
287
+ this.prisma.sttUsage
288
+ .create({
289
+ data: {
290
+ stt_tenant_id: tenantId,
291
+ stt_clinic_id: clinicId,
292
+ stt_duration_seconds: 0, // Meta doesn't provide duration in webhook
293
+ stt_character_count: sttResult.text.length,
294
+ stt_language: clinic.clinic_language || 'tr',
295
+ },
296
+ })
297
+ .catch((err) =>
298
+ this.logger.warn(
299
+ `Failed to log STT usage: ${(err as Error).message}`,
300
+ ),
301
+ );
302
+
303
+ this.logger.debug(
304
+ `STT transcribed audio for chat ${externalChatId}: "${sttResult.text.substring(0, 60)}"`,
305
+ );
306
+ }
307
+ }
308
+ } catch (err: any) {
309
+ this.logger.warn(
310
+ `STT transcription failed for chat ${externalChatId}: ${err.message}. Keeping original media label.`,
311
+ );
312
+ // Keep the original media label text — do not modify message
313
+ }
314
+ }
315
+ }
316
+
317
+ // ── 4. Patient find-or-create ────────────────────────────────────
318
+ const patient = await this.findOrCreatePatient(
319
+ tenantId,
320
+ clinicId,
321
+ sender.phone,
322
+ sender.name,
323
+ channel,
324
+ );
325
+
326
+ // ── 5. Chat find-or-create ───────────────────────────────────────
327
+ const chat = await this.findOrCreateChat(
328
+ tenantId,
329
+ clinicId,
330
+ externalChatId,
331
+ channel,
332
+ patient.patient_id,
333
+ sender,
334
+ payload.phoneNumberId,
335
+ );
336
+
337
+ // ── 6. Blacklist check ───────────────────────────────────────────
338
+ if (clinic.clinic_blacklisted_numbers.includes(sender.phone)) {
339
+ this.logger.debug(`Blacklisted number ${sender.phone} — storing but not triggering`);
340
+ await this.storeEncryptedMessage(
341
+ chat.chat_id,
342
+ chat.chat_active_session_id,
343
+ tenantId,
344
+ message,
345
+ sender,
346
+ );
347
+ return;
348
+ }
349
+
350
+ // ── 7. Encrypt and store message ─────────────────────────────────
351
+ const dbMessage = await this.storeEncryptedMessage(
352
+ chat.chat_id,
353
+ chat.chat_active_session_id,
354
+ tenantId,
355
+ message,
356
+ sender,
357
+ );
358
+
359
+ // Update chat metadata
360
+ const chatUpdates: any = {
361
+ chat_last_message_at: new Date(),
362
+ chat_last_message_preview: message.text.substring(0, 100),
363
+ chat_message_count: { increment: 1 },
364
+ chat_contact_name: sender.name || chat.chat_contact_name,
365
+ };
366
+ // For Instagram, update contact_phone with display name if it's still a raw scoped ID
367
+ if (channel === 'instagram' && sender.name && /^\d+$/.test(chat.chat_contact_phone)) {
368
+ chatUpdates.chat_contact_phone = sender.name;
369
+ }
370
+ await this.prisma.whatsappChat.update({
371
+ where: { chat_id: chat.chat_id },
372
+ data: chatUpdates,
373
+ });
374
+
375
+ // ── 8. Session routing ───────────────────────────────────────────
376
+ if (chat.chat_active_session_id) {
377
+ const session = await this.prisma.whatsappSession.findUnique({
378
+ where: { session_id: chat.chat_active_session_id },
379
+ select: {
380
+ session_id: true,
381
+ session_status: true,
382
+ session_taken_over_by_id: true,
383
+ },
384
+ });
385
+
386
+ if (session) {
387
+ if (session.session_status === 'waiting') {
388
+ // Timer already running — just store (message already stored above)
389
+ this.logger.debug(
390
+ `Session ${session.session_id} is waiting, message stored`,
391
+ );
392
+ } else if (session.session_status === 'active') {
393
+ // Taken over by human? Don't trigger AI
394
+ if (session.session_taken_over_by_id) {
395
+ this.logger.debug(
396
+ `Session ${session.session_id} taken over by human, skipping AI`,
397
+ );
398
+ } else {
399
+ // Enqueue message-buffer job (debounce for multi-message bursts)
400
+ await this.bufferMessage(session.session_id, dbMessage.msg_id);
401
+ }
402
+ }
403
+ // If resolved, fall through to new session creation below is skipped
404
+ // because chat_active_session_id exists
405
+ }
406
+ } else {
407
+ // No active session — start a new one
408
+ const session = await this.startSession(
409
+ chat.chat_id,
410
+ tenantId,
411
+ clinicId,
412
+ );
413
+
414
+ // Link message to the new session
415
+ await this.prisma.whatsappMessage.update({
416
+ where: { msg_id: dbMessage.msg_id },
417
+ data: { msg_session_id: session.session_id },
418
+ });
419
+
420
+ // Update chat active session
421
+ await this.prisma.whatsappChat.update({
422
+ where: { chat_id: chat.chat_id },
423
+ data: { chat_active_session_id: session.session_id },
424
+ });
425
+
426
+ // Send fixed first message if configured (fire-and-forget)
427
+ if (clinic.clinic_fixed_first_message) {
428
+ const receiverIdFromChat = externalChatId.split('@')[1];
429
+ this.sendFixedFirstMessage(
430
+ clinicId,
431
+ sender.phone,
432
+ clinic.clinic_fixed_first_message,
433
+ session.session_id,
434
+ chat.chat_id,
435
+ tenantId,
436
+ receiverIdFromChat,
437
+ ).catch((err) =>
438
+ this.logger.error(
439
+ `Failed to send fixed first message: ${err.message}`,
440
+ ),
441
+ );
442
+ }
443
+ }
444
+
445
+ // ── 9. Emit event for real-time dashboard ────────────────────────
446
+ this.eventEmitter.emit('whatsapp.message.received', {
447
+ chatId: chat.chat_id,
448
+ messageId: dbMessage.msg_id,
449
+ tenantId,
450
+ clinicId,
451
+ });
452
+ }
453
+
454
+ /* ================================================================== */
455
+ /* START SESSION */
456
+ /* ================================================================== */
457
+
458
+ async startSession(chatId: string, tenantId: string, clinicId: string) {
459
+ const clinic = await this.prisma.clinic.findUnique({
460
+ where: { clinic_id: clinicId },
461
+ select: { clinic_grace_period_seconds: true },
462
+ });
463
+
464
+ const gracePeriodMs = (clinic?.clinic_grace_period_seconds ?? 180) * 1000;
465
+ const graceDeadline = new Date(Date.now() + gracePeriodMs);
466
+
467
+ const session = await this.prisma.whatsappSession.create({
468
+ data: {
469
+ session_chat_id: chatId,
470
+ session_tenant_id: tenantId,
471
+ session_clinic_id: clinicId,
472
+ session_status: 'waiting',
473
+ session_grace_deadline: graceDeadline,
474
+ },
475
+ });
476
+
477
+ this.logger.debug(
478
+ `Created session ${session.session_id} (waiting) for chat ${chatId}, grace=${gracePeriodMs}ms`,
479
+ );
480
+
481
+ // Schedule grace period expiry job
482
+ await this.graceQueue.add(
483
+ 'grace-expire',
484
+ { sessionId: session.session_id },
485
+ {
486
+ delay: gracePeriodMs,
487
+ jobId: `grace-${session.session_id}`,
488
+ removeOnComplete: true,
489
+ removeOnFail: 100,
490
+ },
491
+ );
492
+
493
+ return session;
494
+ }
495
+
496
+ /* ================================================================== */
497
+ /* ACTIVATE SESSION — transition waiting → active, connect to AI */
498
+ /* ================================================================== */
499
+
500
+ async activateSession(sessionId: string): Promise<void> {
501
+ const session = await this.prisma.whatsappSession.findUnique({
502
+ where: { session_id: sessionId },
503
+ include: {
504
+ chat: {
505
+ select: {
506
+ chat_id: true,
507
+ chat_external_id: true,
508
+ chat_channel: true,
509
+ chat_contact_phone: true,
510
+ chat_contact_name: true,
511
+ chat_phone_id: true,
512
+ },
513
+ },
514
+ clinic: {
515
+ select: {
516
+ clinic_id: true,
517
+ clinic_tenant_id: true,
518
+ clinic_timezone: true,
519
+ },
520
+ },
521
+ },
522
+ });
523
+
524
+ if (!session || session.session_status !== 'waiting') {
525
+ this.logger.warn(
526
+ `Cannot activate session ${sessionId}: not found or not in waiting status`,
527
+ );
528
+ return;
529
+ }
530
+
531
+ // Transition to active
532
+ await this.prisma.whatsappSession.update({
533
+ where: { session_id: sessionId },
534
+ data: { session_status: 'active' },
535
+ });
536
+
537
+ this.logger.debug(`Session ${sessionId} activated`);
538
+
539
+ // Store WS connection metadata in Redis for awareness across processes
540
+ const redisKey = `ws:conn:${session.chat.chat_id}`;
541
+ await this.redis.hmset(redisKey, {
542
+ sessionId,
543
+ tenantId: session.session_tenant_id,
544
+ clinicId: session.session_clinic_id,
545
+ chatId: session.chat.chat_id,
546
+ activatedAt: new Date().toISOString(),
547
+ });
548
+ await this.redis.expire(redisKey, 3600); // 1 hour TTL
549
+
550
+ // Resolve agent for this clinic and trigger initial AI response
551
+ const agent = await this.resolveAgent(
552
+ session.session_clinic_id,
553
+ session.session_tenant_id,
554
+ );
555
+
556
+ if (agent) {
557
+ // Collect all buffered messages from the waiting period
558
+ const bufferedMessages = await this.prisma.whatsappMessage.findMany({
559
+ where: {
560
+ msg_session_id: sessionId,
561
+ msg_sender: 'contact',
562
+ },
563
+ orderBy: { msg_created_at: 'asc' },
564
+ select: { msg_text: true, msg_media_type: true },
565
+ });
566
+
567
+ const texts: string[] = [];
568
+ for (const msg of bufferedMessages) {
569
+ try {
570
+ const decrypted = this.encryption.decrypt(
571
+ msg.msg_text,
572
+ this.messageEncryptionKey,
573
+ );
574
+ if (decrypted.trim()) {
575
+ texts.push(decrypted);
576
+ } else if (msg.msg_media_type) {
577
+ texts.push(`[${msg.msg_media_type}]`);
578
+ }
579
+ } catch {
580
+ texts.push('[mesaj okunamadi]');
581
+ }
582
+ }
583
+
584
+ const combinedText = texts.join('\n');
585
+
586
+ if (combinedText.trim()) {
587
+ // Fire-and-forget the AI response trigger
588
+ this.triggerAiResponse(
589
+ sessionId,
590
+ session.chat.chat_id,
591
+ combinedText,
592
+ ).catch((err) =>
593
+ this.logger.error(
594
+ `Failed to trigger AI for session ${sessionId}: ${err.message}`,
595
+ err.stack,
596
+ ),
597
+ );
598
+ }
599
+ } else {
600
+ this.logger.warn(
601
+ `No active agent found for clinic ${session.session_clinic_id}, session ${sessionId} won't connect to AI`,
602
+ );
603
+ }
604
+
605
+ this.eventEmitter.emit('whatsapp.session.activated', {
606
+ sessionId,
607
+ chatId: session.session_chat_id,
608
+ tenantId: session.session_tenant_id,
609
+ clinicId: session.session_clinic_id,
610
+ });
611
+
612
+ this.eventEmitter.emit('chat.session.status', {
613
+ sessionId,
614
+ chatId: session.session_chat_id,
615
+ tenantId: session.session_tenant_id,
616
+ status: 'active',
617
+ });
618
+ }
619
+
620
+ /* ================================================================== */
621
+ /* RESOLVE SESSION */
622
+ /* ================================================================== */
623
+
624
+ async resolveSession(
625
+ sessionId: string,
626
+ resolvedBy: 'human' | 'ai_timeout' | 'timeout' | 'manual',
627
+ ): Promise<void> {
628
+ const session = await this.prisma.whatsappSession.findUnique({
629
+ where: { session_id: sessionId },
630
+ include: {
631
+ chat: {
632
+ select: {
633
+ chat_id: true,
634
+ chat_patient_id: true,
635
+ },
636
+ },
637
+ },
638
+ });
639
+
640
+ if (!session) {
641
+ this.logger.warn(`Session ${sessionId} not found for resolve`);
642
+ return;
643
+ }
644
+
645
+ if (session.session_status === 'resolved') {
646
+ this.logger.debug(`Session ${sessionId} already resolved`);
647
+ return;
648
+ }
649
+
650
+ // Close ElevenLabs WebSocket if connected
651
+ this.closeConnection(session.session_chat_id);
652
+
653
+ // Fetch ElevenLabs conversation cost if we have a conversation ID
654
+ let elCost: number | undefined;
655
+ if (session.session_el_conversation_id) {
656
+ try {
657
+ const costData = await this.elevenlabsService.getConversationCost(
658
+ session.session_el_conversation_id,
659
+ );
660
+ elCost = costData?.total_cost_usd ?? costData?.cost;
661
+ } catch (err: any) {
662
+ this.logger.warn(
663
+ `Failed to fetch conversation cost for ${session.session_el_conversation_id}: ${err.message}`,
664
+ );
665
+ }
666
+ }
667
+
668
+ // Update session status
669
+ await this.prisma.whatsappSession.update({
670
+ where: { session_id: sessionId },
671
+ data: {
672
+ session_status: 'resolved',
673
+ session_resolved_by: resolvedBy,
674
+ session_resolved_at: new Date(),
675
+ ...(elCost !== undefined ? { session_el_cost: elCost } : {}),
676
+ },
677
+ });
678
+
679
+ // Clear chat active session
680
+ await this.prisma.whatsappChat.update({
681
+ where: { chat_id: session.session_chat_id },
682
+ data: { chat_active_session_id: null },
683
+ });
684
+
685
+ // Delete Redis WS state
686
+ await this.redis.del(`ws:conn:${session.session_chat_id}`);
687
+
688
+ this.logger.debug(
689
+ `Session ${sessionId} resolved (by: ${resolvedBy})`,
690
+ );
691
+
692
+ // Update patient stats
693
+ if (session.chat.chat_patient_id) {
694
+ this.prisma.patient
695
+ .update({
696
+ where: { patient_id: session.chat.chat_patient_id },
697
+ data: {
698
+ patient_last_contact_at: new Date(),
699
+ patient_total_sessions: { increment: 1 },
700
+ },
701
+ })
702
+ .catch((err) =>
703
+ this.logger.error(
704
+ `Failed to update patient stats: ${err.message}`,
705
+ ),
706
+ );
707
+ }
708
+
709
+ // Schedule post-session jobs
710
+ await Promise.all([
711
+ this.profileUpdateQueue.add(
712
+ 'update-profile',
713
+ {
714
+ sessionId,
715
+ tenantId: session.session_tenant_id,
716
+ chatId: session.session_chat_id,
717
+ },
718
+ { removeOnComplete: true, removeOnFail: 100 },
719
+ ),
720
+ this.sessionLabelsQueue.add(
721
+ 'extract-labels',
722
+ {
723
+ sessionId,
724
+ tenantId: session.session_tenant_id,
725
+ },
726
+ { removeOnComplete: true, removeOnFail: 100 },
727
+ ),
728
+ ]);
729
+
730
+ this.eventEmitter.emit('chat.session.status', {
731
+ sessionId,
732
+ chatId: session.session_chat_id,
733
+ tenantId: session.session_tenant_id,
734
+ status: 'resolved',
735
+ resolvedBy,
736
+ });
737
+
738
+ this.eventEmitter.emit('whatsapp.session.resolved', {
739
+ sessionId,
740
+ chatId: session.session_chat_id,
741
+ tenantId: session.session_tenant_id,
742
+ resolvedBy,
743
+ });
744
+ }
745
+
746
+ /* ================================================================== */
747
+ /* TRIGGER AI RESPONSE — connect to ElevenLabs and send message */
748
+ /* ================================================================== */
749
+
750
+ async triggerAiResponse(
751
+ sessionId: string,
752
+ chatId: string,
753
+ combinedText: string,
754
+ ): Promise<void> {
755
+ // Load session with all needed relations
756
+ const session = await this.prisma.whatsappSession.findUnique({
757
+ where: { session_id: sessionId },
758
+ include: {
759
+ chat: {
760
+ select: {
761
+ chat_id: true,
762
+ chat_external_id: true,
763
+ chat_contact_phone: true,
764
+ chat_contact_name: true,
765
+ chat_phone_id: true,
766
+ chat_channel: true,
767
+ chat_patient_id: true,
768
+ },
769
+ },
770
+ clinic: {
771
+ select: {
772
+ clinic_id: true,
773
+ clinic_tenant_id: true,
774
+ clinic_timezone: true,
775
+ },
776
+ },
777
+ },
778
+ });
779
+
780
+ if (!session) {
781
+ this.logger.warn(`Session ${sessionId} not found for AI trigger`);
782
+ return;
783
+ }
784
+
785
+ if (session.session_status !== 'active') {
786
+ this.logger.debug(
787
+ `Session ${sessionId} not active (${session.session_status}), skipping AI trigger`,
788
+ );
789
+ return;
790
+ }
791
+
792
+ // If session is taken over by a human, skip AI
793
+ if (session.session_taken_over_by_id) {
794
+ this.logger.debug(
795
+ `Session ${sessionId} is taken over, skipping AI trigger`,
796
+ );
797
+ return;
798
+ }
799
+
800
+ // Resolve the agent for this clinic
801
+ const agent = await this.resolveAgent(
802
+ session.session_clinic_id,
803
+ session.session_tenant_id,
804
+ );
805
+
806
+ if (!agent) {
807
+ this.logger.warn(
808
+ `No agent found for clinic ${session.session_clinic_id}, cannot trigger AI`,
809
+ );
810
+ return;
811
+ }
812
+
813
+ // Get or create the WS connection
814
+ const conn = await this.getOrCreateConnection(
815
+ chatId,
816
+ agent.agent_elevenlabs_id,
817
+ session.session_tenant_id,
818
+ sessionId,
819
+ );
820
+
821
+ if (!conn) {
822
+ this.logger.error(
823
+ `Failed to establish WS connection for chat ${chatId}`,
824
+ );
825
+ return;
826
+ }
827
+
828
+ // Wait for the connection to be ready
829
+ try {
830
+ await conn.readyPromise;
831
+ } catch (err: any) {
832
+ this.logger.error(
833
+ `WS connection for chat ${chatId} failed to become ready: ${err.message}`,
834
+ );
835
+ this.closeConnection(chatId);
836
+ return;
837
+ }
838
+
839
+ // Build and send chat context
840
+ const chatContext = await this.buildChatContext(sessionId);
841
+ if (chatContext) {
842
+ this.wsSend(conn, {
843
+ type: 'contextual_update',
844
+ text: chatContext,
845
+ });
846
+ }
847
+
848
+ // Send the user message
849
+ this.wsSend(conn, {
850
+ type: 'user_message',
851
+ text: combinedText,
852
+ });
853
+
854
+ // Update session last interaction
855
+ await this.prisma.whatsappSession.update({
856
+ where: { session_id: sessionId },
857
+ data: { session_el_last_interaction: new Date() },
858
+ });
859
+ }
860
+
861
+ /* ================================================================== */
862
+ /* BUILD CHAT CONTEXT */
863
+ /* ================================================================== */
864
+
865
+ async buildChatContext(sessionId: string): Promise<string> {
866
+ // Fetch all messages in the session
867
+ const messages = await this.prisma.whatsappMessage.findMany({
868
+ where: { msg_session_id: sessionId },
869
+ orderBy: { msg_created_at: 'asc' },
870
+ select: {
871
+ msg_sender: true,
872
+ msg_text: true,
873
+ msg_media_type: true,
874
+ msg_created_at: true,
875
+ },
876
+ });
877
+
878
+ // Fetch operator requests for the session
879
+ const operatorRequests = await this.prisma.operatorRequest.findMany({
880
+ where: { opreq_session_id: sessionId },
881
+ orderBy: { opreq_created_at: 'asc' },
882
+ select: {
883
+ opreq_ref_code: true,
884
+ opreq_status: true,
885
+ opreq_request_message: true,
886
+ opreq_operator_response: true,
887
+ opreq_forwarded_at: true,
888
+ opreq_responded_at: true,
889
+ opreq_created_at: true,
890
+ },
891
+ });
892
+
893
+ // Build timeline entries
894
+ interface TimelineEntry {
895
+ timestamp: Date;
896
+ text: string;
897
+ }
898
+ const timeline: TimelineEntry[] = [];
899
+
900
+ // Add message entries
901
+ for (const msg of messages) {
902
+ let text: string;
903
+ try {
904
+ text = this.encryption.decrypt(msg.msg_text, this.messageEncryptionKey);
905
+ } catch {
906
+ text = msg.msg_media_type ? `[${msg.msg_media_type}]` : '[mesaj okunamadi]';
907
+ }
908
+
909
+ if (!text.trim() && msg.msg_media_type) {
910
+ text = `[${msg.msg_media_type}]`;
911
+ }
912
+
913
+ const senderLabel = this.getSenderLabel(msg.msg_sender);
914
+ timeline.push({
915
+ timestamp: msg.msg_created_at,
916
+ text: `${senderLabel}: ${text}`,
917
+ });
918
+ }
919
+
920
+ // Add operator request events
921
+ for (const req of operatorRequests) {
922
+ timeline.push({
923
+ timestamp: req.opreq_created_at,
924
+ text: `[SISTEM: Doktora yonlendirildi - REF: ${req.opreq_ref_code}${
925
+ req.opreq_request_message ? ` - Not: ${req.opreq_request_message}` : ''
926
+ }]`,
927
+ });
928
+
929
+ if (req.opreq_status === 'responded' && req.opreq_responded_at) {
930
+ timeline.push({
931
+ timestamp: req.opreq_responded_at,
932
+ text: `[SISTEM: Doktor yaniti alindi - REF: ${req.opreq_ref_code} - Yanit: ${req.opreq_operator_response ?? ''}]`,
933
+ });
934
+ }
935
+
936
+ if (req.opreq_status === 'expired') {
937
+ timeline.push({
938
+ timestamp: req.opreq_responded_at ?? req.opreq_created_at,
939
+ text: `[SISTEM: Doktor yanit suresi doldu - REF: ${req.opreq_ref_code}]`,
940
+ });
941
+ }
942
+ }
943
+
944
+ // Sort by timestamp
945
+ timeline.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
946
+
947
+ // Format the context string
948
+ return timeline.map((entry) => entry.text).join('\n');
949
+ }
950
+
951
+ /* ================================================================== */
952
+ /* TAKEOVER — human takes over from AI */
953
+ /* ================================================================== */
954
+
955
+ async takeOver(chatId: string, userId: string): Promise<void> {
956
+ const chat = await this.prisma.whatsappChat.findUnique({
957
+ where: { chat_id: chatId },
958
+ select: { chat_active_session_id: true, chat_tenant_id: true },
959
+ });
960
+
961
+ if (!chat?.chat_active_session_id) {
962
+ this.logger.warn(`No active session on chat ${chatId} for takeover`);
963
+ return;
964
+ }
965
+
966
+ const sessionId = chat.chat_active_session_id;
967
+
968
+ // Close AI WebSocket connection
969
+ this.closeConnection(chatId);
970
+
971
+ // Cancel any pending message buffer jobs for this session
972
+ try {
973
+ const jobs = await this.messageBufferQueue.getJobs([
974
+ 'waiting',
975
+ 'delayed',
976
+ 'active',
977
+ ]);
978
+ for (const job of jobs) {
979
+ if (job.data?.sessionId === sessionId) {
980
+ await job.remove();
981
+ }
982
+ }
983
+ } catch (err: any) {
984
+ this.logger.warn(
985
+ `Failed to cancel buffer jobs for session ${sessionId}: ${err.message}`,
986
+ );
987
+ }
988
+
989
+ await this.prisma.whatsappSession.update({
990
+ where: { session_id: sessionId },
991
+ data: {
992
+ session_taken_over_by_id: userId,
993
+ session_taken_over_at: new Date(),
994
+ },
995
+ });
996
+
997
+ this.logger.log(
998
+ `Session ${sessionId} taken over by user ${userId}`,
999
+ );
1000
+
1001
+ this.eventEmitter.emit('whatsapp.session.takeover', {
1002
+ sessionId,
1003
+ chatId,
1004
+ userId,
1005
+ tenantId: chat.chat_tenant_id,
1006
+ });
1007
+
1008
+ this.eventEmitter.emit('chat.session.status', {
1009
+ sessionId,
1010
+ chatId,
1011
+ tenantId: chat.chat_tenant_id,
1012
+ status: 'active',
1013
+ takenOverBy: userId,
1014
+ });
1015
+ }
1016
+
1017
+ /* ================================================================== */
1018
+ /* RELEASE — release session back to AI */
1019
+ /* ================================================================== */
1020
+
1021
+ async releaseSession(chatId: string): Promise<void> {
1022
+ const chat = await this.prisma.whatsappChat.findUnique({
1023
+ where: { chat_id: chatId },
1024
+ select: {
1025
+ chat_active_session_id: true,
1026
+ chat_tenant_id: true,
1027
+ },
1028
+ });
1029
+
1030
+ if (!chat?.chat_active_session_id) {
1031
+ this.logger.warn(`No active session on chat ${chatId} for release`);
1032
+ return;
1033
+ }
1034
+
1035
+ const sessionId = chat.chat_active_session_id;
1036
+
1037
+ await this.prisma.whatsappSession.update({
1038
+ where: { session_id: sessionId },
1039
+ data: {
1040
+ session_taken_over_by_id: null,
1041
+ session_taken_over_at: null,
1042
+ },
1043
+ });
1044
+
1045
+ this.logger.log(`Session ${sessionId} released back to AI`);
1046
+
1047
+ // AI resumes on the next patient message. No need to proactively
1048
+ // reconnect here — the next incoming message will trigger a buffer
1049
+ // job which calls triggerAiResponse.
1050
+
1051
+ this.eventEmitter.emit('whatsapp.session.released', {
1052
+ sessionId,
1053
+ chatId,
1054
+ });
1055
+
1056
+ this.eventEmitter.emit('chat.session.status', {
1057
+ sessionId,
1058
+ chatId,
1059
+ tenantId: chat.chat_tenant_id,
1060
+ status: 'active',
1061
+ takenOverBy: null,
1062
+ });
1063
+ }
1064
+
1065
+ /* ================================================================== */
1066
+ /* SEND HUMAN MESSAGE */
1067
+ /* ================================================================== */
1068
+
1069
+ async sendHumanMessage(
1070
+ chatId: string,
1071
+ userId: string,
1072
+ text: string,
1073
+ ): Promise<void> {
1074
+ const chat = await this.prisma.whatsappChat.findUnique({
1075
+ where: { chat_id: chatId },
1076
+ include: {
1077
+ clinic: {
1078
+ select: {
1079
+ clinic_id: true,
1080
+ clinic_tenant_id: true,
1081
+ },
1082
+ },
1083
+ },
1084
+ });
1085
+
1086
+ if (!chat) {
1087
+ this.logger.warn(`Chat ${chatId} not found for sending message`);
1088
+ return;
1089
+ }
1090
+
1091
+ // Extract recipient and receiver from external ID (format: sender@receiver)
1092
+ const [recipientId, receiverId] = chat.chat_external_id.split('@');
1093
+
1094
+ // Send message via Meta Business API (WhatsApp or Instagram)
1095
+ const metaResult = chat.chat_channel === 'instagram'
1096
+ ? await this.sendViaInstagram(chat.clinic.clinic_id, recipientId, text, receiverId)
1097
+ : await this.sendViaMeta(chat.clinic.clinic_id, recipientId, text, receiverId);
1098
+
1099
+ // Resolve user name
1100
+ let senderName = '';
1101
+ try {
1102
+ const user = await this.prisma.user.findUnique({
1103
+ where: { user_id: userId },
1104
+ select: { user_first_name: true, user_last_name: true },
1105
+ });
1106
+ if (user) {
1107
+ senderName = [user.user_first_name, user.user_last_name]
1108
+ .filter(Boolean)
1109
+ .join(' ');
1110
+ }
1111
+ } catch {
1112
+ // Non-critical
1113
+ }
1114
+
1115
+ // Persist the message
1116
+ const encryptedText = this.encryption.encrypt(
1117
+ text,
1118
+ this.messageEncryptionKey,
1119
+ );
1120
+
1121
+ const msg = await this.prisma.whatsappMessage.create({
1122
+ data: {
1123
+ msg_chat_id: chatId,
1124
+ msg_session_id: chat.chat_active_session_id,
1125
+ msg_tenant_id: chat.chat_tenant_id,
1126
+ msg_external_id: metaResult?.messages?.[0]?.id ?? null,
1127
+ msg_sender: 'human',
1128
+ msg_sender_name: senderName,
1129
+ msg_sender_user_id: userId,
1130
+ msg_text: encryptedText,
1131
+ },
1132
+ });
1133
+
1134
+ // Update chat metadata
1135
+ await this.prisma.whatsappChat.update({
1136
+ where: { chat_id: chatId },
1137
+ data: {
1138
+ chat_last_message_at: new Date(),
1139
+ chat_last_message_preview: text.substring(0, 100),
1140
+ chat_message_count: { increment: 1 },
1141
+ },
1142
+ });
1143
+
1144
+ this.eventEmitter.emit('whatsapp.message.sent', {
1145
+ chatId,
1146
+ messageId: msg.msg_id,
1147
+ tenantId: chat.chat_tenant_id,
1148
+ });
1149
+
1150
+ this.eventEmitter.emit('chat.message.created', {
1151
+ chatId,
1152
+ messageId: msg.msg_id,
1153
+ tenantId: chat.chat_tenant_id,
1154
+ sender: 'human',
1155
+ });
1156
+ }
1157
+
1158
+ /* ================================================================== */
1159
+ /* SEND HUMAN MEDIA */
1160
+ /* ================================================================== */
1161
+
1162
+ async sendHumanMedia(
1163
+ chatId: string,
1164
+ userId: string,
1165
+ buffer: Buffer,
1166
+ mimeType: string,
1167
+ filename: string,
1168
+ caption?: string,
1169
+ ): Promise<void> {
1170
+ const chat = await this.prisma.whatsappChat.findUnique({
1171
+ where: { chat_id: chatId },
1172
+ include: {
1173
+ clinic: { select: { clinic_id: true, clinic_tenant_id: true } },
1174
+ },
1175
+ });
1176
+
1177
+ if (!chat) {
1178
+ this.logger.warn(`Chat ${chatId} not found for sending media`);
1179
+ return;
1180
+ }
1181
+
1182
+ const [recipientId, receiverId] = chat.chat_external_id.split('@');
1183
+ const mediaType = this.resolveMediaType(mimeType);
1184
+
1185
+ let metaResult: any = null;
1186
+
1187
+ if (chat.chat_channel === 'instagram') {
1188
+ // Instagram: upload isn't supported via API, use URL-based approach
1189
+ // Store in MinIO first, then send via URL
1190
+ const minioKey = `chat-media-out/${chat.chat_tenant_id}/${Date.now()}-${filename}`;
1191
+ await this.minio.putObject(minioKey, buffer, mimeType);
1192
+ const mediaUrl = await this.minio.presignedGetObject(minioKey, 86400);
1193
+
1194
+ const metaConn = await this.getInstagramConnection(chat.clinic.clinic_id, receiverId);
1195
+ if (metaConn) {
1196
+ metaResult = await this.metaInstagram.sendMedia(
1197
+ metaConn.pageId,
1198
+ recipientId,
1199
+ mediaUrl,
1200
+ metaConn.token,
1201
+ );
1202
+ }
1203
+ } else {
1204
+ // WhatsApp: upload to Meta first, then send by ID
1205
+ const metaConn = await this.getWhatsAppConnection(chat.clinic.clinic_id, receiverId);
1206
+ if (metaConn) {
1207
+ const mediaId = await this.metaWhatsApp.uploadMedia(
1208
+ metaConn.phoneNumberId,
1209
+ buffer,
1210
+ mimeType,
1211
+ filename,
1212
+ metaConn.token,
1213
+ );
1214
+ metaResult = await this.metaWhatsApp.sendMediaById(
1215
+ metaConn.phoneNumberId,
1216
+ recipientId,
1217
+ mediaType,
1218
+ mediaId,
1219
+ caption,
1220
+ metaConn.token,
1221
+ filename,
1222
+ );
1223
+ }
1224
+ }
1225
+
1226
+ // Resolve sender name
1227
+ let senderName = '';
1228
+ try {
1229
+ const user = await this.prisma.user.findUnique({
1230
+ where: { user_id: userId },
1231
+ select: { user_first_name: true, user_last_name: true },
1232
+ });
1233
+ if (user) senderName = [user.user_first_name, user.user_last_name].filter(Boolean).join(' ');
1234
+ } catch {}
1235
+
1236
+ // Store in MinIO for display
1237
+ const minioKey = `chat-media/${chat.chat_tenant_id}/${metaResult?.messages?.[0]?.id || Date.now()}`;
1238
+ this.minio.putObject(minioKey, buffer, mimeType).catch(() => {});
1239
+
1240
+ // Persist message
1241
+ const encryptedCaption = this.encryption.encrypt(
1242
+ caption || '',
1243
+ this.messageEncryptionKey,
1244
+ );
1245
+
1246
+ const msg = await this.prisma.whatsappMessage.create({
1247
+ data: {
1248
+ msg_chat_id: chatId,
1249
+ msg_session_id: chat.chat_active_session_id,
1250
+ msg_tenant_id: chat.chat_tenant_id,
1251
+ msg_external_id: metaResult?.messages?.[0]?.id ?? null,
1252
+ msg_sender: 'human',
1253
+ msg_sender_name: senderName,
1254
+ msg_sender_user_id: userId,
1255
+ msg_text: encryptedCaption,
1256
+ msg_media_type: mediaType,
1257
+ msg_media_key: minioKey,
1258
+ msg_media_mime: mimeType,
1259
+ msg_media_filename: filename,
1260
+ },
1261
+ });
1262
+
1263
+ await this.prisma.whatsappChat.update({
1264
+ where: { chat_id: chatId },
1265
+ data: {
1266
+ chat_last_message_at: new Date(),
1267
+ chat_last_message_preview: caption || `[${mediaType}]`,
1268
+ chat_message_count: { increment: 1 },
1269
+ },
1270
+ });
1271
+
1272
+ this.eventEmitter.emit('chat.message.created', {
1273
+ chatId,
1274
+ messageId: msg.msg_id,
1275
+ tenantId: chat.chat_tenant_id,
1276
+ sender: 'human',
1277
+ });
1278
+ }
1279
+
1280
+ private resolveMediaType(mimeType: string): 'image' | 'video' | 'document' | 'audio' {
1281
+ if (mimeType.startsWith('image/')) return 'image';
1282
+ if (mimeType.startsWith('video/')) return 'video';
1283
+ if (mimeType.startsWith('audio/')) return 'audio';
1284
+ return 'document';
1285
+ }
1286
+
1287
+ private async getWhatsAppConnection(clinicId: string, metaPhoneNumberId?: string) {
1288
+ const where: any = { meta_clinic_id: clinicId, meta_type: 'whatsapp', meta_is_active: true };
1289
+ if (metaPhoneNumberId) where.meta_phone_number_id = metaPhoneNumberId;
1290
+
1291
+ const conn = await this.prisma.clinicMetaConnection.findFirst({
1292
+ where,
1293
+ select: { meta_phone_number_id: true, meta_encrypted_access_token: true },
1294
+ });
1295
+ if (!conn?.meta_phone_number_id || !conn?.meta_encrypted_access_token) return null;
1296
+
1297
+ const token = this.tokenEncryptionKey
1298
+ ? this.encryption.decrypt(conn.meta_encrypted_access_token, this.tokenEncryptionKey)
1299
+ : conn.meta_encrypted_access_token;
1300
+ return { phoneNumberId: conn.meta_phone_number_id, token };
1301
+ }
1302
+
1303
+ private async getInstagramConnection(clinicId: string, igPageId?: string) {
1304
+ const where: any = { meta_clinic_id: clinicId, meta_type: 'instagram', meta_is_active: true };
1305
+ if (igPageId) where.meta_instagram_page_id = igPageId;
1306
+
1307
+ const conn = await this.prisma.clinicMetaConnection.findFirst({
1308
+ where,
1309
+ select: { meta_instagram_page_id: true, meta_facebook_page_id: true, meta_encrypted_access_token: true },
1310
+ });
1311
+ const pageId = conn?.meta_facebook_page_id || conn?.meta_instagram_page_id;
1312
+ if (!pageId || !conn?.meta_encrypted_access_token) return null;
1313
+
1314
+ const token = this.tokenEncryptionKey
1315
+ ? this.encryption.decrypt(conn.meta_encrypted_access_token, this.tokenEncryptionKey)
1316
+ : conn.meta_encrypted_access_token;
1317
+ return { pageId, token };
1318
+ }
1319
+
1320
+ /* ================================================================== */
1321
+ /* INJECT OPERATOR RESPONSE — called by OperatorService */
1322
+ /* ================================================================== */
1323
+
1324
+ async injectOperatorResponse(
1325
+ sessionId: string,
1326
+ refCode: string,
1327
+ responseText: string,
1328
+ ): Promise<void> {
1329
+ const session = await this.prisma.whatsappSession.findUnique({
1330
+ where: { session_id: sessionId },
1331
+ select: {
1332
+ session_id: true,
1333
+ session_status: true,
1334
+ session_chat_id: true,
1335
+ session_tenant_id: true,
1336
+ session_clinic_id: true,
1337
+ },
1338
+ });
1339
+
1340
+ if (!session || session.session_status !== 'active') {
1341
+ this.logger.warn(
1342
+ `Cannot inject operator response for session ${sessionId}: not active`,
1343
+ );
1344
+ return;
1345
+ }
1346
+
1347
+ const conn = this.wsConnections.get(session.session_chat_id);
1348
+ const contextText =
1349
+ `[SISTEM: Doktor yaniti alindi - REF: ${refCode} - Yanit: ${responseText}]`;
1350
+
1351
+ if (conn && conn.ws.readyState === WebSocket.OPEN) {
1352
+ // WS still alive — send contextual update
1353
+ this.wsSend(conn, {
1354
+ type: 'contextual_update',
1355
+ text: contextText,
1356
+ });
1357
+ this.logger.debug(
1358
+ `Injected operator response for ${refCode} into live WS`,
1359
+ );
1360
+ } else {
1361
+ // WS dead — recreate connection with full chat context
1362
+ this.logger.debug(
1363
+ `WS dead for chat ${session.session_chat_id}, recreating for operator response`,
1364
+ );
1365
+
1366
+ const agent = await this.resolveAgent(
1367
+ session.session_clinic_id,
1368
+ session.session_tenant_id,
1369
+ );
1370
+
1371
+ if (!agent) {
1372
+ this.logger.error(
1373
+ `No agent found when recreating WS for operator response`,
1374
+ );
1375
+ return;
1376
+ }
1377
+
1378
+ const newConn = await this.getOrCreateConnection(
1379
+ session.session_chat_id,
1380
+ agent.agent_elevenlabs_id,
1381
+ session.session_tenant_id,
1382
+ sessionId,
1383
+ );
1384
+
1385
+ if (!newConn) return;
1386
+
1387
+ try {
1388
+ await newConn.readyPromise;
1389
+ } catch {
1390
+ this.logger.error(`Failed to reconnect WS for operator response`);
1391
+ return;
1392
+ }
1393
+
1394
+ // Send full chat history as context
1395
+ const chatContext = await this.buildChatContext(sessionId);
1396
+ if (chatContext) {
1397
+ this.wsSend(newConn, { type: 'contextual_update', text: chatContext });
1398
+ }
1399
+
1400
+ // Send the operator response as a user message so the AI acts on it
1401
+ this.wsSend(newConn, {
1402
+ type: 'contextual_update',
1403
+ text: contextText,
1404
+ });
1405
+ }
1406
+ }
1407
+
1408
+ /* ================================================================== */
1409
+ /* EVENT LISTENERS */
1410
+ /* ================================================================== */
1411
+
1412
+ /** Handle buffered messages from MessageBufferProcessor */
1413
+ @OnEvent('whatsapp.messages.buffered')
1414
+ async onMessagesBuffered(payload: {
1415
+ sessionId: string;
1416
+ chatId: string;
1417
+ tenantId: string;
1418
+ clinicId: string;
1419
+ combinedText: string;
1420
+ messageCount: number;
1421
+ }): Promise<void> {
1422
+ this.logger.debug(
1423
+ `Buffered ${payload.messageCount} messages for session ${payload.sessionId}`,
1424
+ );
1425
+ await this.triggerAiResponse(
1426
+ payload.sessionId,
1427
+ payload.chatId,
1428
+ payload.combinedText,
1429
+ );
1430
+ }
1431
+
1432
+ /* ================================================================== */
1433
+ /* ELEVENLABS WEBSOCKET MANAGEMENT */
1434
+ /* ================================================================== */
1435
+
1436
+ /**
1437
+ * Get an existing WS connection or create a new one.
1438
+ * Returns null if the connection cannot be established.
1439
+ */
1440
+ private async getOrCreateConnection(
1441
+ chatId: string,
1442
+ agentId: string,
1443
+ tenantId: string,
1444
+ sessionId: string,
1445
+ ): Promise<WsConnection | null> {
1446
+ // Check if we already have a live connection
1447
+ const existing = this.wsConnections.get(chatId);
1448
+ if (existing && existing.ws.readyState === WebSocket.OPEN) {
1449
+ return existing;
1450
+ }
1451
+
1452
+ // Clean up stale connection if any
1453
+ if (existing) {
1454
+ this.closeConnection(chatId);
1455
+ }
1456
+
1457
+ try {
1458
+ // Detect reconnection: session already has a conversation_id
1459
+ const sessionData = await this.prisma.whatsappSession.findUnique({
1460
+ where: { session_id: sessionId },
1461
+ select: {
1462
+ session_el_conversation_id: true,
1463
+ session_el_prev_conversation_ids: true,
1464
+ },
1465
+ });
1466
+ const isReconnect = !!sessionData?.session_el_conversation_id;
1467
+
1468
+ // Get signed URL from ElevenLabs
1469
+ const { signed_url } = await this.elevenlabsService.getSignedUrl(agentId);
1470
+
1471
+ // Build patient summary for dynamic variables
1472
+ const patientSummary = await this.getPatientSummary(sessionId);
1473
+
1474
+ // Build continuation directive for reconnection
1475
+ let continuationDirective = '';
1476
+ if (isReconnect) {
1477
+ try {
1478
+ const chatContext = await this.buildChatContext(sessionId);
1479
+ // Find last AI message for the directive
1480
+ const lastAiMsg = await this.prisma.whatsappMessage.findFirst({
1481
+ where: {
1482
+ msg_session_id: sessionId,
1483
+ msg_sender: 'ai',
1484
+ },
1485
+ orderBy: { msg_created_at: 'desc' },
1486
+ select: { msg_text: true },
1487
+ });
1488
+
1489
+ let lastAiText = '';
1490
+ if (lastAiMsg) {
1491
+ try {
1492
+ lastAiText = this.encryption.decrypt(
1493
+ lastAiMsg.msg_text,
1494
+ this.messageEncryptionKey,
1495
+ );
1496
+ } catch {
1497
+ lastAiText = '[mesaj okunamadi]';
1498
+ }
1499
+ }
1500
+
1501
+ continuationDirective = [
1502
+ 'ONEMLI -- KONUSMA DEVAMI:',
1503
+ 'Bu hasta ile onceden baslamis bir konusma var. Teknik bir baglanti yenilenmesi yasandi.',
1504
+ 'Kendini TANITMA. Selam VERME. Hosgeldin DEME. Konusmaya kaldigin yerden DEVAM ET.',
1505
+ lastAiText
1506
+ ? `En son gonderdigin mesaj: "${lastAiText}"`
1507
+ : '',
1508
+ '',
1509
+ 'Konusma gecmisi:',
1510
+ chatContext,
1511
+ ]
1512
+ .filter(Boolean)
1513
+ .join('\n');
1514
+
1515
+ // Push old conversation_id to prev_conversation_ids
1516
+ const prevIds = sessionData.session_el_prev_conversation_ids ?? [];
1517
+ prevIds.push(sessionData.session_el_conversation_id!);
1518
+ await this.prisma.whatsappSession.update({
1519
+ where: { session_id: sessionId },
1520
+ data: {
1521
+ session_el_prev_conversation_ids: prevIds,
1522
+ session_el_conversation_id: null, // Will be set when new WS is ready
1523
+ },
1524
+ });
1525
+
1526
+ this.logger.debug(
1527
+ `WS reconnect for session ${sessionId}, continuation directive built (${continuationDirective.length} chars)`,
1528
+ );
1529
+ } catch (err: any) {
1530
+ this.logger.warn(
1531
+ `Failed to build continuation directive for session ${sessionId}: ${err.message}`,
1532
+ );
1533
+ }
1534
+ }
1535
+
1536
+ // Create ready promise for synchronization
1537
+ let readyResolve!: () => void;
1538
+ let readyReject!: (err: Error) => void;
1539
+ const readyPromise = new Promise<void>((resolve, reject) => {
1540
+ readyResolve = resolve;
1541
+ readyReject = reject;
1542
+ });
1543
+
1544
+ const ws = new WebSocket(`${signed_url}&textOnly=true`);
1545
+
1546
+ const conn: WsConnection = {
1547
+ ws,
1548
+ agentId,
1549
+ tenantId,
1550
+ sessionId,
1551
+ chatId,
1552
+ isReady: false,
1553
+ readyPromise,
1554
+ readyResolve,
1555
+ };
1556
+
1557
+ this.wsConnections.set(chatId, conn);
1558
+
1559
+ // Capture reconnect state and context for the open handler closure
1560
+ const _isReconnect = isReconnect;
1561
+ const _continuationDirective = continuationDirective;
1562
+
1563
+ // Setup event handlers
1564
+ ws.on('open', () => {
1565
+ this.logger.debug(`WS opened for chat ${chatId} (reconnect=${_isReconnect})`);
1566
+
1567
+ // Send conversation initiation data
1568
+ ws.send(
1569
+ JSON.stringify({
1570
+ type: 'conversation_initiation_client_data',
1571
+ conversation_config_override: {},
1572
+ dynamic_variables: {
1573
+ user_summary: patientSummary,
1574
+ current_date: new Date().toISOString().split('T')[0],
1575
+ continuation_directive: _continuationDirective,
1576
+ },
1577
+ }),
1578
+ );
1579
+ });
1580
+
1581
+ ws.on('message', (data) => {
1582
+ this.handleWsMessage(chatId, data.toString()).catch((err) =>
1583
+ this.logger.error(
1584
+ `WS message handler error for chat ${chatId}: ${err.message}`,
1585
+ err.stack,
1586
+ ),
1587
+ );
1588
+ });
1589
+
1590
+ ws.on('close', (code, reason) => {
1591
+ this.logger.debug(
1592
+ `WS closed for chat ${chatId}: code=${code} reason=${reason.toString()}`,
1593
+ );
1594
+ this.wsConnections.delete(chatId);
1595
+ this.redis
1596
+ .del(`ws:conn:${chatId}`)
1597
+ .catch(() => { /* ignore */ });
1598
+
1599
+ if (!conn.isReady) {
1600
+ readyReject(new Error(`WS closed before ready: ${code}`));
1601
+ }
1602
+ });
1603
+
1604
+ ws.on('error', (err) => {
1605
+ this.logger.error(
1606
+ `WS error for chat ${chatId}: ${err.message}`,
1607
+ );
1608
+ if (!conn.isReady) {
1609
+ readyReject(err);
1610
+ }
1611
+ });
1612
+
1613
+ // Set a timeout for the ready state
1614
+ const readyTimeout = setTimeout(() => {
1615
+ if (!conn.isReady) {
1616
+ readyReject(new Error('WS ready timeout (15s)'));
1617
+ this.closeConnection(chatId);
1618
+ }
1619
+ }, 15_000);
1620
+
1621
+ // Clean up timeout when ready
1622
+ readyPromise.then(() => clearTimeout(readyTimeout)).catch(() => clearTimeout(readyTimeout));
1623
+
1624
+ return conn;
1625
+ } catch (err: any) {
1626
+ this.logger.error(
1627
+ `Failed to create WS connection for chat ${chatId}: ${err.message}`,
1628
+ err.stack,
1629
+ );
1630
+ return null;
1631
+ }
1632
+ }
1633
+
1634
+ /** Close a WS connection and clean up. */
1635
+ private closeConnection(chatId: string): void {
1636
+ const conn = this.wsConnections.get(chatId);
1637
+ if (!conn) return;
1638
+
1639
+ try {
1640
+ if (
1641
+ conn.ws.readyState === WebSocket.OPEN ||
1642
+ conn.ws.readyState === WebSocket.CONNECTING
1643
+ ) {
1644
+ conn.ws.close(1000, 'session_ended');
1645
+ }
1646
+ } catch {
1647
+ // Ignore close errors
1648
+ }
1649
+
1650
+ this.wsConnections.delete(chatId);
1651
+ this.redis.del(`ws:conn:${chatId}`).catch(() => { /* ignore */ });
1652
+
1653
+ this.logger.debug(`Closed WS connection for chat ${chatId}`);
1654
+ }
1655
+
1656
+ /** Route incoming WS messages from ElevenLabs. */
1657
+ private async handleWsMessage(
1658
+ chatId: string,
1659
+ rawData: string,
1660
+ ): Promise<void> {
1661
+ let msg: any;
1662
+ try {
1663
+ msg = JSON.parse(rawData);
1664
+ } catch {
1665
+ this.logger.warn(`Non-JSON WS message for chat ${chatId}: ${rawData.substring(0, 100)}`);
1666
+ return;
1667
+ }
1668
+
1669
+ const conn = this.wsConnections.get(chatId);
1670
+ if (!conn) {
1671
+ this.logger.warn(
1672
+ `Received WS message for unknown chat ${chatId}: type=${msg.type}`,
1673
+ );
1674
+ return;
1675
+ }
1676
+
1677
+ switch (msg.type) {
1678
+ case 'conversation_initiation_metadata': {
1679
+ // Connection is ready — store conversation ID
1680
+ conn.conversationId = msg.conversation_id;
1681
+ conn.isReady = true;
1682
+ conn.readyResolve();
1683
+
1684
+ // Persist the conversation ID on the session
1685
+ if (msg.conversation_id) {
1686
+ await this.prisma.whatsappSession.update({
1687
+ where: { session_id: conn.sessionId },
1688
+ data: {
1689
+ session_el_conversation_id: msg.conversation_id,
1690
+ session_el_conversation_created: new Date(),
1691
+ },
1692
+ });
1693
+ }
1694
+
1695
+ this.logger.debug(
1696
+ `WS ready for chat ${chatId}, conversationId=${msg.conversation_id}`,
1697
+ );
1698
+ break;
1699
+ }
1700
+
1701
+ case 'agent_response': {
1702
+ // AI responded — send to patient and store
1703
+ const responseText = msg.agent_response_text ?? msg.text ?? '';
1704
+ if (!responseText.trim()) break;
1705
+
1706
+ await this.handleAgentResponse(conn, responseText);
1707
+ break;
1708
+ }
1709
+
1710
+ case 'client_tool_call': {
1711
+ // Delegate to tool registry via the WS adapter
1712
+ await this.handleClientToolCall(conn, msg);
1713
+ break;
1714
+ }
1715
+
1716
+ case 'ping': {
1717
+ // Respond with pong to keep alive
1718
+ this.wsSend(conn, { type: 'pong' });
1719
+ break;
1720
+ }
1721
+
1722
+ case 'user_transcript':
1723
+ case 'internal_tentative_agent_response':
1724
+ // Informational — ignore
1725
+ break;
1726
+
1727
+ default:
1728
+ this.logger.debug(
1729
+ `Unhandled WS message type for chat ${chatId}: ${msg.type}`,
1730
+ );
1731
+ }
1732
+ }
1733
+
1734
+ /** Handle an AI agent_response: send to patient + store. */
1735
+ private async handleAgentResponse(
1736
+ conn: WsConnection,
1737
+ responseText: string,
1738
+ ): Promise<void> {
1739
+ const { chatId, sessionId, tenantId } = conn;
1740
+
1741
+ // Send to patient via Meta API (fire-and-forget)
1742
+ this.sendAiResponseToPatient(chatId, responseText).catch((err) =>
1743
+ this.logger.error(
1744
+ `Failed to send AI response for chat ${chatId}: ${err.message}`,
1745
+ ),
1746
+ );
1747
+
1748
+ // Encrypt and store as AI message
1749
+ const encryptedText = this.encryption.encrypt(
1750
+ responseText,
1751
+ this.messageEncryptionKey,
1752
+ );
1753
+
1754
+ const msg = await this.prisma.whatsappMessage.create({
1755
+ data: {
1756
+ msg_chat_id: chatId,
1757
+ msg_session_id: sessionId,
1758
+ msg_tenant_id: tenantId,
1759
+ msg_sender: 'ai',
1760
+ msg_sender_name: 'AI',
1761
+ msg_text: encryptedText,
1762
+ },
1763
+ });
1764
+
1765
+ // Update chat metadata
1766
+ await this.prisma.whatsappChat.update({
1767
+ where: { chat_id: chatId },
1768
+ data: {
1769
+ chat_last_message_at: new Date(),
1770
+ chat_last_message_preview: responseText.substring(0, 100),
1771
+ chat_message_count: { increment: 1 },
1772
+ },
1773
+ });
1774
+
1775
+ // Update session message count and last interaction
1776
+ await this.prisma.whatsappSession.update({
1777
+ where: { session_id: sessionId },
1778
+ data: {
1779
+ session_message_count: { increment: 1 },
1780
+ session_el_last_interaction: new Date(),
1781
+ },
1782
+ });
1783
+
1784
+ // Emit for real-time dashboard
1785
+ this.eventEmitter.emit('whatsapp.message.sent', {
1786
+ chatId,
1787
+ messageId: msg.msg_id,
1788
+ tenantId,
1789
+ });
1790
+
1791
+ this.eventEmitter.emit('chat.message.created', {
1792
+ chatId,
1793
+ messageId: msg.msg_id,
1794
+ tenantId,
1795
+ sender: 'ai',
1796
+ });
1797
+ }
1798
+
1799
+ /** Handle a client_tool_call from ElevenLabs. */
1800
+ private async handleClientToolCall(
1801
+ conn: WsConnection,
1802
+ msg: any,
1803
+ ): Promise<void> {
1804
+ const { chatId, sessionId, tenantId } = conn;
1805
+
1806
+ // Load chat for context
1807
+ const chat = await this.prisma.whatsappChat.findUnique({
1808
+ where: { chat_id: chatId },
1809
+ select: {
1810
+ chat_contact_phone: true,
1811
+ chat_contact_name: true,
1812
+ chat_clinic_id: true,
1813
+ chat_channel: true,
1814
+ chat_patient_id: true,
1815
+ chat_phone_id: true,
1816
+ },
1817
+ });
1818
+
1819
+ if (!chat) {
1820
+ this.logger.error(`Chat ${chatId} not found for tool call`);
1821
+ // Send error result back to ElevenLabs
1822
+ this.wsSend(conn, {
1823
+ type: 'client_tool_result',
1824
+ tool_call_id: msg.tool_call_id,
1825
+ result: 'Internal error: chat not found',
1826
+ is_error: true,
1827
+ });
1828
+ return;
1829
+ }
1830
+
1831
+ // Build tool context
1832
+ const toolCtx: ToolContext = {
1833
+ tenantId,
1834
+ clinicId: chat.chat_clinic_id,
1835
+ patientPhone: chat.chat_contact_phone,
1836
+ patientName: chat.chat_contact_name ?? '',
1837
+ patientId: chat.chat_patient_id ?? undefined,
1838
+ channel: chat.chat_channel as 'whatsapp' | 'instagram',
1839
+ channelMetadata: {
1840
+ chatId,
1841
+ sessionId,
1842
+ conversationId: conn.conversationId,
1843
+ agentId: conn.agentId,
1844
+ phoneNumberId: chat.chat_phone_id ?? undefined,
1845
+ },
1846
+ };
1847
+
1848
+ // Delegate to the WS adapter which handles the tool registry
1849
+ const result = await this.wsAdapter.handleToolCall(
1850
+ {
1851
+ tool_call_id: msg.tool_call_id,
1852
+ tool_name: msg.tool_name,
1853
+ parameters: msg.parameters ?? {},
1854
+ },
1855
+ toolCtx,
1856
+ );
1857
+
1858
+ // Send result back to ElevenLabs
1859
+ this.wsSend(conn, result);
1860
+ }
1861
+
1862
+ /* ================================================================== */
1863
+ /* PRIVATE HELPERS */
1864
+ /* ================================================================== */
1865
+
1866
+ /** Check if message text matches any system message pattern. */
1867
+ private isSystemMessage(text: string): boolean {
1868
+ if (!text || !text.trim()) return false;
1869
+ return SYSTEM_MESSAGE_PATTERNS.some((pattern) => pattern.test(text));
1870
+ }
1871
+
1872
+ /**
1873
+ * Handle echo (isSender) messages.
1874
+ * Returns true if the message was handled (should be skipped).
1875
+ */
1876
+ private async handleEchoMessage(
1877
+ tenantId: string,
1878
+ externalChatId: string,
1879
+ msgExternalId: string,
1880
+ ): Promise<boolean> {
1881
+ // Check if this is a message we sent via our API (match by external ID)
1882
+ const ourMessage = await this.prisma.whatsappMessage.findFirst({
1883
+ where: {
1884
+ msg_external_id: msgExternalId,
1885
+ msg_tenant_id: tenantId,
1886
+ msg_sender: { in: ['ai', 'human', 'system'] },
1887
+ },
1888
+ });
1889
+
1890
+ if (ourMessage) {
1891
+ // This is an echo of a message we sent — skip entirely
1892
+ this.logger.debug(`Echo of our message ${msgExternalId}, skipping`);
1893
+ return true;
1894
+ }
1895
+
1896
+ // This was sent by a real person from the business phone (not via our API).
1897
+ // Check if there's a session in 'waiting' — resolve as 'human'
1898
+ const chat = await this.prisma.whatsappChat.findUnique({
1899
+ where: {
1900
+ chat_tenant_id_chat_external_id: {
1901
+ chat_tenant_id: tenantId,
1902
+ chat_external_id: externalChatId,
1903
+ },
1904
+ },
1905
+ select: { chat_active_session_id: true },
1906
+ });
1907
+
1908
+ if (chat?.chat_active_session_id) {
1909
+ const session = await this.prisma.whatsappSession.findUnique({
1910
+ where: { session_id: chat.chat_active_session_id },
1911
+ select: { session_status: true, session_id: true },
1912
+ });
1913
+
1914
+ if (session?.session_status === 'waiting') {
1915
+ this.logger.log(
1916
+ `Human replied from business phone, resolving session ${session.session_id}`,
1917
+ );
1918
+ await this.resolveSession(session.session_id, 'human');
1919
+ }
1920
+ }
1921
+
1922
+ return true;
1923
+ }
1924
+
1925
+ /** Store a system message (audit trail only, no session trigger). */
1926
+ private async storeSystemMessage(
1927
+ tenantId: string,
1928
+ externalChatId: string,
1929
+ message: IncomingMessagePayload['message'],
1930
+ sender: IncomingMessagePayload['sender'],
1931
+ ): Promise<void> {
1932
+ // Find the chat (don't create one for system messages)
1933
+ const chat = await this.prisma.whatsappChat.findUnique({
1934
+ where: {
1935
+ chat_tenant_id_chat_external_id: {
1936
+ chat_tenant_id: tenantId,
1937
+ chat_external_id: externalChatId,
1938
+ },
1939
+ },
1940
+ select: { chat_id: true, chat_active_session_id: true },
1941
+ });
1942
+
1943
+ if (!chat) {
1944
+ // No existing chat — don't create one for a system message
1945
+ return;
1946
+ }
1947
+
1948
+ const encryptedText = this.encryption.encrypt(
1949
+ message.text,
1950
+ this.messageEncryptionKey,
1951
+ );
1952
+
1953
+ await this.prisma.whatsappMessage.create({
1954
+ data: {
1955
+ msg_chat_id: chat.chat_id,
1956
+ msg_session_id: chat.chat_active_session_id,
1957
+ msg_tenant_id: tenantId,
1958
+ msg_external_id: message.externalId,
1959
+ msg_sender: 'system',
1960
+ msg_sender_name: sender.name ?? '',
1961
+ msg_text: encryptedText,
1962
+ },
1963
+ });
1964
+ }
1965
+
1966
+ /** Encrypt and store an incoming message. */
1967
+ private async storeEncryptedMessage(
1968
+ chatId: string,
1969
+ sessionId: string | null,
1970
+ tenantId: string,
1971
+ message: IncomingMessagePayload['message'],
1972
+ sender: IncomingMessagePayload['sender'],
1973
+ ) {
1974
+ const encryptedText = this.encryption.encrypt(
1975
+ message.text,
1976
+ this.messageEncryptionKey,
1977
+ );
1978
+
1979
+ const encryptedQuoted = message.quotedText
1980
+ ? this.encryption.encrypt(message.quotedText, this.messageEncryptionKey)
1981
+ : null;
1982
+
1983
+ return this.prisma.whatsappMessage.create({
1984
+ data: {
1985
+ msg_chat_id: chatId,
1986
+ msg_session_id: sessionId,
1987
+ msg_tenant_id: tenantId,
1988
+ msg_external_id: message.externalId,
1989
+ msg_sender: 'contact',
1990
+ msg_sender_name: sender.name ?? '',
1991
+ msg_text: encryptedText,
1992
+ msg_media_type: message.mediaType,
1993
+ msg_media_key: message.mediaKey,
1994
+ msg_media_mime: message.mediaMime,
1995
+ msg_media_filename: message.mediaFilename,
1996
+ msg_ad_context: message.adContext as any,
1997
+ msg_metadata: message.metadata as any,
1998
+ msg_quoted_text: encryptedQuoted,
1999
+ },
2000
+ });
2001
+ }
2002
+
2003
+ private async findOrCreatePatient(
2004
+ tenantId: string,
2005
+ clinicId: string,
2006
+ phone: string,
2007
+ name?: string,
2008
+ channel: 'whatsapp' | 'instagram' = 'whatsapp',
2009
+ ) {
2010
+ let patient = await this.prisma.patient.findUnique({
2011
+ where: {
2012
+ patient_tenant_id_patient_phone: {
2013
+ patient_tenant_id: tenantId,
2014
+ patient_phone: phone,
2015
+ },
2016
+ },
2017
+ });
2018
+
2019
+ if (!patient) {
2020
+ const displayName = name || 'Bilinmiyor';
2021
+ patient = await this.prisma.patient.create({
2022
+ data: {
2023
+ patient_tenant_id: tenantId,
2024
+ patient_clinic_id: clinicId,
2025
+ patient_phone: phone,
2026
+ patient_display_name: displayName,
2027
+ patient_first_name: name ?? '',
2028
+ patient_last_name: '',
2029
+ patient_source: channel,
2030
+ patient_last_contact_at: new Date(),
2031
+ },
2032
+ });
2033
+ this.logger.debug(
2034
+ `Created new patient ${patient.patient_id} for ${phone}`,
2035
+ );
2036
+ } else {
2037
+ // Update last contact + name if we now have a better one
2038
+ const updates: any = { patient_last_contact_at: new Date() };
2039
+ if (name && (!patient.patient_display_name || patient.patient_display_name === 'Bilinmiyor' || patient.patient_display_name === patient.patient_phone)) {
2040
+ updates.patient_display_name = name;
2041
+ updates.patient_first_name = name;
2042
+ }
2043
+ await this.prisma.patient.update({
2044
+ where: { patient_id: patient.patient_id },
2045
+ data: updates,
2046
+ });
2047
+ }
2048
+
2049
+ return patient;
2050
+ }
2051
+
2052
+ private async findOrCreateChat(
2053
+ tenantId: string,
2054
+ clinicId: string,
2055
+ externalChatId: string,
2056
+ channel: 'whatsapp' | 'instagram',
2057
+ patientId: string,
2058
+ sender: { phone: string; name?: string },
2059
+ phoneNumberId?: string,
2060
+ ) {
2061
+ let chat = await this.prisma.whatsappChat.findUnique({
2062
+ where: {
2063
+ chat_tenant_id_chat_external_id: {
2064
+ chat_tenant_id: tenantId,
2065
+ chat_external_id: externalChatId,
2066
+ },
2067
+ },
2068
+ });
2069
+
2070
+ if (!chat) {
2071
+ // Resolve phone number record — phoneNumberId may be Meta's internal ID
2072
+ let phoneId: string | null = null;
2073
+ if (phoneNumberId) {
2074
+ // Try direct match first (actual phone number string)
2075
+ let phone = await this.prisma.phoneNumber.findFirst({
2076
+ where: { phone_tenant_id: tenantId, phone_number: phoneNumberId },
2077
+ select: { phone_id: true },
2078
+ });
2079
+ // Fall back: resolve via ClinicMetaConnection (Meta phone number ID)
2080
+ if (!phone) {
2081
+ const metaConn = await this.prisma.clinicMetaConnection.findFirst({
2082
+ where: { meta_phone_number_id: phoneNumberId, meta_is_active: true },
2083
+ select: { meta_display_phone: true },
2084
+ });
2085
+ if (metaConn?.meta_display_phone) {
2086
+ phone = await this.prisma.phoneNumber.findFirst({
2087
+ where: { phone_tenant_id: tenantId, phone_number: metaConn.meta_display_phone },
2088
+ select: { phone_id: true },
2089
+ });
2090
+ }
2091
+ }
2092
+ phoneId = phone?.phone_id ?? null;
2093
+ }
2094
+
2095
+ chat = await this.prisma.whatsappChat.create({
2096
+ data: {
2097
+ chat_tenant_id: tenantId,
2098
+ chat_clinic_id: clinicId,
2099
+ chat_patient_id: patientId,
2100
+ chat_channel: channel,
2101
+ chat_external_id: externalChatId,
2102
+ chat_phone_id: phoneId,
2103
+ chat_contact_name: sender.name || 'Bilinmiyor',
2104
+ chat_contact_phone: sender.name && channel === 'instagram' ? sender.name : sender.phone,
2105
+ },
2106
+ });
2107
+ this.logger.debug(
2108
+ `Created new chat ${chat.chat_id} for external ${externalChatId}`,
2109
+ );
2110
+ }
2111
+
2112
+ return chat;
2113
+ }
2114
+
2115
+ private static readonly AGENT_CACHE_PREFIX = 'agent_config:';
2116
+ private static readonly AGENT_CACHE_TTL = 300; // 5 minutes
2117
+
2118
+ /** Resolve the agent for a clinic (cached in Redis). */
2119
+ private async resolveAgent(
2120
+ clinicId: string,
2121
+ tenantId: string,
2122
+ ): Promise<ResolvedAgent | null> {
2123
+ const cacheKey = `${WhatsAppAgentService.AGENT_CACHE_PREFIX}${tenantId}:${clinicId}`;
2124
+
2125
+ // Check cache
2126
+ const cached = await this.redis.get(cacheKey);
2127
+ if (cached) {
2128
+ if (cached === 'null') return null;
2129
+ return JSON.parse(cached) as ResolvedAgent;
2130
+ }
2131
+
2132
+ // DB lookup
2133
+ const agent = await this.prisma.agent.findFirst({
2134
+ where: {
2135
+ agent_is_active: true,
2136
+ agent_status: 'production',
2137
+ OR: [
2138
+ { agent_clinic_id: clinicId },
2139
+ { agent_tenant_id: tenantId, agent_clinic_id: null },
2140
+ ],
2141
+ },
2142
+ orderBy: {
2143
+ agent_clinic_id: { sort: 'desc', nulls: 'last' },
2144
+ },
2145
+ select: {
2146
+ agent_id: true,
2147
+ agent_elevenlabs_id: true,
2148
+ agent_name: true,
2149
+ agent_system_prompt: true,
2150
+ agent_ai_provider: true,
2151
+ agent_ai_model: true,
2152
+ agent_language: true,
2153
+ agent_greeting: true,
2154
+ agent_temperature: true,
2155
+ agent_max_tokens: true,
2156
+ agent_enabled_tools: true,
2157
+ agent_channels: true,
2158
+ agent_extra_config: true,
2159
+ },
2160
+ });
2161
+
2162
+ // Cache result (even null to avoid repeated DB misses)
2163
+ await this.redis.setex(
2164
+ cacheKey,
2165
+ WhatsAppAgentService.AGENT_CACHE_TTL,
2166
+ agent ? JSON.stringify(agent) : 'null',
2167
+ );
2168
+
2169
+ return agent;
2170
+ }
2171
+
2172
+ /** Invalidate agent cache for a tenant (call after agent config changes). */
2173
+ async invalidateAgentCache(tenantId: string): Promise<void> {
2174
+ const keys = await this.redis.keys(`${WhatsAppAgentService.AGENT_CACHE_PREFIX}${tenantId}:*`);
2175
+ if (keys.length > 0) {
2176
+ await this.redis.del(...keys);
2177
+ this.logger.debug(`Invalidated ${keys.length} agent cache entries for tenant ${tenantId}`);
2178
+ }
2179
+ }
2180
+
2181
+ /** Get the decrypted patient summary for dynamic variables. */
2182
+ private async getPatientSummary(sessionId: string): Promise<string> {
2183
+ try {
2184
+ const session = await this.prisma.whatsappSession.findUnique({
2185
+ where: { session_id: sessionId },
2186
+ select: {
2187
+ chat: {
2188
+ select: {
2189
+ chat_patient_id: true,
2190
+ },
2191
+ },
2192
+ },
2193
+ });
2194
+
2195
+ if (!session?.chat.chat_patient_id) return '';
2196
+
2197
+ const patient = await this.prisma.patient.findUnique({
2198
+ where: { patient_id: session.chat.chat_patient_id },
2199
+ select: {
2200
+ patient_display_name: true,
2201
+ patient_phone: true,
2202
+ patient_ai_summary: true,
2203
+ patient_total_sessions: true,
2204
+ patient_tags: true,
2205
+ },
2206
+ });
2207
+
2208
+ if (!patient) return '';
2209
+
2210
+ const parts: string[] = [
2211
+ `Hasta: ${patient.patient_display_name}`,
2212
+ `Telefon: ${patient.patient_phone}`,
2213
+ `Toplam gorusme: ${patient.patient_total_sessions}`,
2214
+ ];
2215
+
2216
+ if (patient.patient_tags.length > 0) {
2217
+ parts.push(`Etiketler: ${patient.patient_tags.join(', ')}`);
2218
+ }
2219
+
2220
+ // Decrypt AI summary if available
2221
+ if (patient.patient_ai_summary && this.patientSummaryKey) {
2222
+ try {
2223
+ const summary = this.encryption.decrypt(
2224
+ patient.patient_ai_summary,
2225
+ this.patientSummaryKey,
2226
+ );
2227
+ if (summary.trim()) {
2228
+ parts.push(`Onceki notlar: ${summary}`);
2229
+ }
2230
+ } catch {
2231
+ // Non-critical — skip summary if decryption fails
2232
+ }
2233
+ }
2234
+
2235
+ return parts.join('\n');
2236
+ } catch (err: any) {
2237
+ this.logger.warn(
2238
+ `Failed to build patient summary for session ${sessionId}: ${err.message}`,
2239
+ );
2240
+ return '';
2241
+ }
2242
+ }
2243
+
2244
+ /** Send AI response text to the patient via Meta Business API. */
2245
+ private async sendAiResponseToPatient(
2246
+ chatId: string,
2247
+ text: string,
2248
+ ): Promise<void> {
2249
+ const chat = await this.prisma.whatsappChat.findUnique({
2250
+ where: { chat_id: chatId },
2251
+ select: {
2252
+ chat_external_id: true,
2253
+ chat_clinic_id: true,
2254
+ chat_channel: true,
2255
+ },
2256
+ });
2257
+
2258
+ if (!chat) return;
2259
+
2260
+ const [recipientId, receiverId] = chat.chat_external_id.split('@');
2261
+
2262
+ if (chat.chat_channel === 'instagram') {
2263
+ await this.sendViaInstagram(chat.chat_clinic_id, recipientId, text, receiverId);
2264
+ } else {
2265
+ await this.sendViaMeta(chat.chat_clinic_id, recipientId, text, receiverId);
2266
+ }
2267
+ }
2268
+
2269
+ /** Send a text message via Meta Business API. Resolves the access token. */
2270
+ private async sendViaMeta(
2271
+ clinicId: string,
2272
+ recipientPhone: string,
2273
+ text: string,
2274
+ metaPhoneNumberId?: string,
2275
+ ): Promise<any> {
2276
+ // Find the exact Meta connection by phone number ID, or fall back to any active one
2277
+ const where: any = {
2278
+ meta_clinic_id: clinicId,
2279
+ meta_type: 'whatsapp',
2280
+ meta_is_active: true,
2281
+ };
2282
+ if (metaPhoneNumberId) {
2283
+ where.meta_phone_number_id = metaPhoneNumberId;
2284
+ }
2285
+ const metaConnection = await this.prisma.clinicMetaConnection.findFirst({
2286
+ where,
2287
+ select: {
2288
+ meta_phone_number_id: true,
2289
+ meta_encrypted_access_token: true,
2290
+ },
2291
+ });
2292
+
2293
+ if (
2294
+ !metaConnection?.meta_phone_number_id ||
2295
+ !metaConnection?.meta_encrypted_access_token
2296
+ ) {
2297
+ this.logger.warn(
2298
+ `No active Meta WhatsApp connection for clinic ${clinicId}`,
2299
+ );
2300
+ return null;
2301
+ }
2302
+
2303
+ let accessToken: string;
2304
+ try {
2305
+ accessToken = this.tokenEncryptionKey
2306
+ ? this.encryption.decrypt(
2307
+ metaConnection.meta_encrypted_access_token,
2308
+ this.tokenEncryptionKey,
2309
+ )
2310
+ : metaConnection.meta_encrypted_access_token;
2311
+ } catch {
2312
+ this.logger.error(
2313
+ `Failed to decrypt Meta access token for clinic ${clinicId}`,
2314
+ );
2315
+ return null;
2316
+ }
2317
+
2318
+ return this.metaWhatsApp.sendMessage(
2319
+ metaConnection.meta_phone_number_id,
2320
+ recipientPhone,
2321
+ text,
2322
+ accessToken,
2323
+ );
2324
+ }
2325
+
2326
+ /** Send a text message via Instagram Messenger API. */
2327
+ private async sendViaInstagram(
2328
+ clinicId: string,
2329
+ recipientId: string,
2330
+ text: string,
2331
+ igPageId?: string,
2332
+ ): Promise<any> {
2333
+ const where: any = {
2334
+ meta_clinic_id: clinicId,
2335
+ meta_type: 'instagram',
2336
+ meta_is_active: true,
2337
+ };
2338
+ if (igPageId) {
2339
+ where.meta_instagram_page_id = igPageId;
2340
+ }
2341
+ const metaConnection = await this.prisma.clinicMetaConnection.findFirst({
2342
+ where,
2343
+ select: {
2344
+ meta_instagram_page_id: true,
2345
+ meta_facebook_page_id: true,
2346
+ meta_encrypted_access_token: true,
2347
+ },
2348
+ });
2349
+
2350
+ // Instagram messaging API is called on the Facebook Page ID
2351
+ const pageId = metaConnection?.meta_facebook_page_id || metaConnection?.meta_instagram_page_id;
2352
+
2353
+ if (
2354
+ !pageId ||
2355
+ !metaConnection?.meta_encrypted_access_token
2356
+ ) {
2357
+ this.logger.warn(
2358
+ `No active Meta Instagram connection for clinic ${clinicId}`,
2359
+ );
2360
+ return null;
2361
+ }
2362
+
2363
+ let accessToken: string;
2364
+ try {
2365
+ accessToken = this.tokenEncryptionKey
2366
+ ? this.encryption.decrypt(
2367
+ metaConnection.meta_encrypted_access_token,
2368
+ this.tokenEncryptionKey,
2369
+ )
2370
+ : metaConnection.meta_encrypted_access_token;
2371
+ } catch {
2372
+ this.logger.error(
2373
+ `Failed to decrypt Meta Instagram token for clinic ${clinicId}`,
2374
+ );
2375
+ return null;
2376
+ }
2377
+
2378
+ return this.metaInstagram.sendMessage(
2379
+ pageId,
2380
+ recipientId,
2381
+ text,
2382
+ accessToken,
2383
+ );
2384
+ }
2385
+
2386
+ /** Send the fixed first message configured on the clinic. */
2387
+ private async sendFixedFirstMessage(
2388
+ clinicId: string,
2389
+ recipientPhone: string,
2390
+ fixedMessage: string,
2391
+ sessionId: string,
2392
+ chatId: string,
2393
+ tenantId: string,
2394
+ metaPhoneNumberId?: string,
2395
+ ): Promise<void> {
2396
+ const metaResult = await this.sendViaMeta(
2397
+ clinicId,
2398
+ recipientPhone,
2399
+ fixedMessage,
2400
+ metaPhoneNumberId,
2401
+ );
2402
+
2403
+ if (!metaResult) return;
2404
+
2405
+ // Store the fixed message
2406
+ const encryptedText = this.encryption.encrypt(
2407
+ fixedMessage,
2408
+ this.messageEncryptionKey,
2409
+ );
2410
+
2411
+ const msg = await this.prisma.whatsappMessage.create({
2412
+ data: {
2413
+ msg_chat_id: chatId,
2414
+ msg_session_id: sessionId,
2415
+ msg_tenant_id: tenantId,
2416
+ msg_external_id: metaResult?.messages?.[0]?.id ?? null,
2417
+ msg_sender: 'system',
2418
+ msg_sender_name: 'System',
2419
+ msg_text: encryptedText,
2420
+ },
2421
+ });
2422
+
2423
+ await this.prisma.whatsappChat.update({
2424
+ where: { chat_id: chatId },
2425
+ data: {
2426
+ chat_last_message_at: new Date(),
2427
+ chat_last_message_preview: fixedMessage.substring(0, 100),
2428
+ chat_message_count: { increment: 1 },
2429
+ },
2430
+ });
2431
+
2432
+ this.eventEmitter.emit('chat.message.created', {
2433
+ chatId,
2434
+ messageId: msg.msg_id,
2435
+ tenantId,
2436
+ sender: 'system',
2437
+ });
2438
+ }
2439
+
2440
+ /** Enqueue a message-buffer BullMQ job. */
2441
+ private async bufferMessage(
2442
+ sessionId: string,
2443
+ messageId: string,
2444
+ ): Promise<void> {
2445
+ await this.messageBufferQueue.add(
2446
+ 'buffer',
2447
+ { sessionId, messageId },
2448
+ {
2449
+ delay: 1500, // 1.5s debounce for multi-message bursts
2450
+ removeOnComplete: true,
2451
+ removeOnFail: 100,
2452
+ },
2453
+ );
2454
+ }
2455
+
2456
+ /** Get a human-readable sender label. */
2457
+ private getSenderLabel(
2458
+ sender: string,
2459
+ ): string {
2460
+ switch (sender) {
2461
+ case 'contact':
2462
+ return 'Hasta';
2463
+ case 'ai':
2464
+ return 'AI';
2465
+ case 'human':
2466
+ return 'Operator';
2467
+ case 'system':
2468
+ return 'Sistem';
2469
+ default:
2470
+ return sender;
2471
+ }
2472
+ }
2473
+
2474
+ /** Check if a tenant has a specific feature enabled. */
2475
+ private async isTenantFeatureEnabled(
2476
+ tenantId: string,
2477
+ feature: string,
2478
+ ): Promise<boolean> {
2479
+ try {
2480
+ const tenant = await this.prisma.tenant.findUnique({
2481
+ where: { tenant_id: tenantId },
2482
+ select: { tenant_enabled_features: true },
2483
+ });
2484
+ return tenant?.tenant_enabled_features?.includes(feature) ?? false;
2485
+ } catch {
2486
+ return false;
2487
+ }
2488
+ }
2489
+
2490
+ /** Send a JSON message over a WS connection. */
2491
+ private wsSend(conn: WsConnection, data: Record<string, any>): void {
2492
+ if (conn.ws.readyState !== WebSocket.OPEN) {
2493
+ this.logger.warn(
2494
+ `Cannot send WS message for chat ${conn.chatId}: not open (state=${conn.ws.readyState})`,
2495
+ );
2496
+ return;
2497
+ }
2498
+ try {
2499
+ conn.ws.send(JSON.stringify(data));
2500
+ } catch (err: any) {
2501
+ this.logger.error(
2502
+ `Failed to send WS message for chat ${conn.chatId}: ${err.message}`,
2503
+ );
2504
+ }
2505
+ }
2506
+ }