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,917 @@
1
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+ import mongoose from 'mongoose';
3
+ import config from '../../config';
4
+ import logger from '../../utils/logger';
5
+ import Tenant from '../tenants/tenant.model';
6
+ import Agent from '../agents/agent.model';
7
+ import WhatsAppChat from '../whatsapp/whatsapp-chat.model';
8
+ import WhatsAppSession from '../whatsapp/whatsapp-session.model';
9
+ import WhatsAppMessage from '../whatsapp/whatsapp-message.model';
10
+ import WhatsAppContactProfile from '../whatsapp/whatsapp-contact-profile.model';
11
+ import whatsappAgentService from '../../services/whatsapp-agent.service';
12
+ import unipileService from '../../services/unipile.service';
13
+ import elevenlabsService from '../../services/elevenlabs.service';
14
+ import OperatorRequest from '../whatsapp/operator-request.model';
15
+ import SttUsage from '../whatsapp/stt-usage.model';
16
+
17
+ interface UnipileWebhookPayload {
18
+ account_id: string;
19
+ account_type: string;
20
+ event: string;
21
+ chat_id: string;
22
+ message_id: string;
23
+ message: string;
24
+ content_type?: string;
25
+ timestamp: string;
26
+ is_sender: boolean;
27
+ is_event?: boolean;
28
+ event_type?: number;
29
+ hidden?: boolean;
30
+ sender: {
31
+ attendee_id: string;
32
+ attendee_name: string;
33
+ attendee_provider_id: string;
34
+ attendee_specifics?: { phone_number?: string };
35
+ };
36
+ attendees: Array<{
37
+ attendee_id: string;
38
+ attendee_name: string;
39
+ attendee_provider_id: string;
40
+ attendee_specifics?: { phone_number?: string };
41
+ }>;
42
+ account_info: {
43
+ type: string;
44
+ phone_number?: string;
45
+ [key: string]: unknown;
46
+ };
47
+ ad_context?: {
48
+ ad?: { title?: string | null; body?: string | null; source_url?: string | null };
49
+ source?: string;
50
+ app?: string;
51
+ } | null;
52
+ quoted_text?: string | null;
53
+ }
54
+
55
+ export default async function unipileWebhookRoutes(fastify: FastifyInstance): Promise<void> {
56
+ // Accept any content type Unipile might send
57
+ fastify.addContentTypeParser('*', { parseAs: 'string' }, (_request, payload, done) => {
58
+ try {
59
+ done(null, JSON.parse(payload as string));
60
+ } catch {
61
+ done(null, payload);
62
+ }
63
+ });
64
+
65
+ fastify.post('/', async (request: FastifyRequest, reply: FastifyReply) => {
66
+ // Validate shared secret — accept if it matches either provider's secret
67
+ const secret = request.headers['x-unipile-secret'] as string;
68
+ if (secret) {
69
+ const validSecrets = [config.unipile.webhookSecret, config.baileys.webhookSecret].filter(Boolean);
70
+ if (validSecrets.length > 0 && !validSecrets.includes(secret)) {
71
+ logger.warn('Invalid webhook secret');
72
+ return reply.status(401).send({ error: 'Invalid secret' });
73
+ }
74
+ }
75
+
76
+ const payload = request.body as UnipileWebhookPayload;
77
+
78
+ // Respond immediately
79
+ reply.status(200).send({ status: 'received' });
80
+
81
+ // Process in background
82
+ processUnipileWebhook(payload).catch(error => {
83
+ logger.error('Unipile webhook processing failed', {
84
+ error: (error as Error).message,
85
+ stack: (error as Error).stack,
86
+ });
87
+ });
88
+ });
89
+ }
90
+
91
+ function isValidPhoneNumber(phone: string): boolean {
92
+ return /^\+?\d{10,15}$/.test(phone.replace(/[\s\-()]/g, ''));
93
+ }
94
+
95
+ /**
96
+ * Create a new WhatsAppSession for a chat and link it.
97
+ */
98
+ async function createSession(
99
+ chat: InstanceType<typeof WhatsAppChat>,
100
+ tenantId: string,
101
+ gracePeriodSeconds: number,
102
+ ): Promise<InstanceType<typeof WhatsAppSession>> {
103
+ const deadline = new Date(Date.now() + gracePeriodSeconds * 1000);
104
+ const session = await WhatsAppSession.create({
105
+ chat_id: chat._id,
106
+ tenant_id: tenantId,
107
+ status: 'waiting',
108
+ grace_deadline: deadline,
109
+ started_at: new Date(),
110
+ });
111
+
112
+ chat.active_session_id = session._id;
113
+ if (chat.is_closed) chat.is_closed = false;
114
+ if (chat.deleted_at) chat.deleted_at = null;
115
+ await chat.save();
116
+
117
+ // Increment profile session count
118
+ if (chat.contact_profile_id) {
119
+ await WhatsAppContactProfile.updateOne(
120
+ { _id: chat.contact_profile_id },
121
+ { $inc: { total_sessions: 1 } },
122
+ );
123
+ }
124
+
125
+ return session;
126
+ }
127
+
128
+ const UNIPILE_MEDIA_PLACEHOLDER = '-- Unipile cannot display this type of message yet';
129
+ const FRIENDLY_MEDIA_TEXT = 'Bu mesajı görüntülemek için WhatsApp uygulamasını kontrol edin';
130
+
131
+ async function processUnipileWebhook(payload: UnipileWebhookPayload): Promise<void> {
132
+ logger.info('Unipile webhook payload', { payload: JSON.stringify(payload).substring(0, 1000) });
133
+
134
+ // Only handle message_received events
135
+ if (payload.event !== 'message_received') {
136
+ logger.debug('Skipping non-message Unipile event', { event: payload.event });
137
+ return;
138
+ }
139
+
140
+ // Only handle WhatsApp
141
+ if (payload.account_type !== 'WHATSAPP') {
142
+ logger.debug('Skipping non-WhatsApp Unipile event', { type: payload.account_type });
143
+ return;
144
+ }
145
+
146
+ // Skip empty messages
147
+ if (!payload.message?.trim()) {
148
+ logger.debug('Ignoring empty message');
149
+ return;
150
+ }
151
+
152
+ // ─── System message detection (two layers) ───────────────
153
+ // Layer 1: Unipile structural fields (most reliable)
154
+ // Layer 2: Text pattern fallback (for event_type=0 / unsupported events)
155
+ // System messages are stored for visibility but never trigger AI or sessions.
156
+
157
+ const SYSTEM_MESSAGE_PATTERNS = [
158
+ // EN — Voice/video calls
159
+ /^(incoming|missed|declined|unanswered|group) (voice|video) call/i,
160
+ /^voice call(,| ended| answered)/i,
161
+ /^video call(,| ended| answered)/i,
162
+ // TR — Voice/video calls
163
+ /^(gelen|cevaps[ıi]z|reddedilen|cevaplanmayan|grup) (sesli|g[öo]r[üu]nt[üu]l[üu]) arama/i,
164
+ /^sesli arama (sona erdi|cevapland)/i,
165
+ /^g[öo]r[üu]nt[üu]l[üu] arama (sona erdi|cevapland)/i,
166
+
167
+ // EN — Message deletion
168
+ /^this message was deleted/i,
169
+ /^you deleted this message/i,
170
+ // TR — Message deletion
171
+ /^bu mesaj silindi/i,
172
+ /^bu mesaj[ıi] sildiniz/i,
173
+ /taraf[ıi]ndan silindi/i,
174
+
175
+ // EN — Encryption & security
176
+ /^messages and calls are end-to-end encrypted/i,
177
+ /security code.*(changed|verified)/i,
178
+ // TR — Encryption & security
179
+ /mesajlar ve aramalar u[çc]tan u[çc]a [şs]ifrelidir/i,
180
+ /g[üu]venlik kodu.* de[gğ]i[şs]ti/i,
181
+
182
+ // EN — Disappearing messages
183
+ /turned (on|off) disappearing messages/i,
184
+ /disappearing messages are (on|off)/i,
185
+ // TR — Disappearing messages
186
+ /s[üu]reli mesajlar[ıi] (a[çc]t|kapatt)/i,
187
+ /s[üu]reli mesajlar (a[çc][ıi]k|kapal[ıi])/i,
188
+
189
+ // EN — Waiting / pending
190
+ /^waiting for this message/i,
191
+ // TR — Waiting / pending
192
+ /^bu mesaj bekleniyor/i,
193
+
194
+ // EN — Live location
195
+ /is sharing live location/i,
196
+ /stopped sharing live location/i,
197
+ // TR — Live location
198
+ /canl[ıi] konum payla[şs]/i,
199
+
200
+ // EN — Payment
201
+ /^payment (sent|received)/i,
202
+ /^payment request (sent|received)/i,
203
+ // TR — Payment
204
+ /^[öo]deme (g[öo]nderildi|al[ıi]nd[ıi])/i,
205
+ /^[öo]deme iste[gğ]i (g[öo]nderildi|al[ıi]nd[ıi])/i,
206
+
207
+ // EN — Business account
208
+ /this chat is with a business account/i,
209
+ /uses a secure service from Meta/i,
210
+ // TR — Business account
211
+ /bu sohbet bir i[şs]letme hesab[ıi]yla/i,
212
+
213
+ // EN — Group management
214
+ /^you were (added|removed)/i,
215
+ /^you('re| are) now an admin/i,
216
+ /^you('re| are) no longer an admin/i,
217
+ /is now an admin$/i,
218
+ /is no longer an admin$/i,
219
+ /joined using this group's invite link/i,
220
+ /pinned a message$/i,
221
+ /unpinned a message$/i,
222
+ /changed the (subject|group description)/i,
223
+ /changed this group's icon/i,
224
+ /only admins can (send messages|edit)/i,
225
+ /all participants can (send messages|edit)/i,
226
+ // TR — Group management
227
+ /art[ıi]k bir y[öo]netici/i,
228
+ /art[ıi]k y[öo]netici de[gğ]il/i,
229
+ /grubun davet ba[gğ]lant[ıi]s[ıi]n[ıi] kullanarak kat[ıi]ld[ıi]/i,
230
+ /bir mesaj[ıi] sabitledi/i,
231
+ /sabitlenen mesaj[ıi] kald[ıi]rd[ıi]/i,
232
+
233
+ // EN — View once
234
+ /sent a view once (message|photo|video)/i,
235
+ // TR — View once
236
+ /bir kez g[öo]r[üu]nt[üu]lenen (mesaj|foto|video)/i,
237
+
238
+ // EN — Old version
239
+ /using an old(er)? version of WhatsApp/i,
240
+ /need(s)? to update WhatsApp/i,
241
+ ];
242
+
243
+ const trimmedMessage = payload.message.trim();
244
+ const isSystemMessage =
245
+ payload.is_event === true ||
246
+ payload.hidden === true ||
247
+ SYSTEM_MESSAGE_PATTERNS.some(pattern => pattern.test(trimmedMessage));
248
+
249
+ if (isSystemMessage) {
250
+ logger.info('System message detected — AI will be skipped', {
251
+ chat_id: payload.chat_id,
252
+ message: trimmedMessage.substring(0, 80),
253
+ is_event: payload.is_event,
254
+ event_type: payload.event_type,
255
+ hidden: payload.hidden,
256
+ });
257
+ }
258
+
259
+ // Detect Unipile media placeholder and replace with friendly text.
260
+ // Baileys now sends descriptive labels like "[Kullanıcı fotoğraf gönderdi]"
261
+ // which pass through to the AI. Only the legacy generic placeholder gets replaced.
262
+ const isLegacyMediaPlaceholder = payload.message === UNIPILE_MEDIA_PLACEHOLDER;
263
+ if (isLegacyMediaPlaceholder) {
264
+ payload.message = FRIENDLY_MEDIA_TEXT;
265
+ }
266
+
267
+ // Find tenant by unipile_account_id
268
+ const tenant = await Tenant.findOne({
269
+ 'settings.whatsapp_agent.unipile_account_id': payload.account_id,
270
+ enabled_features: 'whatsapp_agent',
271
+ });
272
+
273
+ if (!tenant) {
274
+ logger.warn('No tenant found for Unipile account', { account_id: payload.account_id });
275
+ return;
276
+ }
277
+
278
+ // ─── Voice message transcription (STT) ──────────────────────
279
+ if (
280
+ payload.content_type === 'audioMessage' &&
281
+ !payload.is_sender &&
282
+ tenant.enabled_features.includes('voice_transcription') &&
283
+ payload.message_id
284
+ ) {
285
+ try {
286
+ const provider = unipileService.getProviderForTenant(tenant);
287
+ const { audio_base64 } = await unipileService.downloadAudio(provider, payload.chat_id, payload.message_id);
288
+ const audioBuffer = Buffer.from(audio_base64, 'base64');
289
+ const { text: transcript, durationSeconds, detectedLanguage } = await elevenlabsService.speechToText(audioBuffer);
290
+ if (transcript.trim()) {
291
+ payload.message = transcript;
292
+ payload.content_type = 'transcribedAudio';
293
+ logger.info('Voice message transcribed', {
294
+ chatId: payload.chat_id,
295
+ transcriptLength: transcript.length,
296
+ durationSeconds,
297
+ detectedLanguage,
298
+ preview: transcript.substring(0, 100),
299
+ });
300
+
301
+ // Log STT usage
302
+ SttUsage.create({
303
+ tenant_id: tenant._id,
304
+ chat_id: payload.chat_id,
305
+ message_id: payload.message_id,
306
+ duration_seconds: durationSeconds,
307
+ character_count: transcript.length,
308
+ language: detectedLanguage,
309
+ }).catch(() => {});
310
+ }
311
+ } catch (err) {
312
+ logger.error('Voice message transcription failed, using label fallback', {
313
+ chatId: payload.chat_id,
314
+ error: (err as Error).message,
315
+ });
316
+ }
317
+ }
318
+
319
+ // ─── Operator / forwarding reply detection ──────────────────
320
+ // Check for REF code from operator_phone or conversation_forwarding_phone
321
+ const operatorSettings = tenant.settings?.whatsapp_agent;
322
+ const knownOperatorPhones: string[] = [];
323
+ if (operatorSettings?.operator_workflow_enabled && operatorSettings.operator_phone) {
324
+ knownOperatorPhones.push(operatorSettings.operator_phone.replace(/[^0-9]/g, ''));
325
+ }
326
+ if (tenant.enabled_features.includes('conversation_forwarding') && operatorSettings?.conversation_forwarding_phone) {
327
+ const cfPhone = operatorSettings.conversation_forwarding_phone.replace(/[^0-9]/g, '');
328
+ if (!knownOperatorPhones.includes(cfPhone)) knownOperatorPhones.push(cfPhone);
329
+ }
330
+
331
+ if (knownOperatorPhones.length > 0) {
332
+ // Check for REF code in the message text OR in the quoted (replied-to) message
333
+ const refMatch = payload.message.trim().match(/\b([A-Z]-[A-Z0-9]{4})\b/)
334
+ || (payload.quoted_text?.match(/\b([A-Z]-[A-Z0-9]{4})\b/) ?? null);
335
+
336
+ if (refMatch) {
337
+ // Scenario 1: incoming message from a known operator/forwarding phone
338
+ if (!payload.is_sender) {
339
+ const senderPhone = payload.attendees?.[0]?.attendee_specifics?.phone_number
340
+ || payload.attendees?.[0]?.attendee_provider_id || '';
341
+ const normalizedSender = senderPhone.replace(/[^0-9]/g, '');
342
+
343
+ if (normalizedSender && knownOperatorPhones.includes(normalizedSender)) {
344
+ logger.info('Operator message detected (incoming)', { phone: senderPhone, message: payload.message.substring(0, 100) });
345
+ await handleOperatorReply(tenant, payload);
346
+ return;
347
+ }
348
+ }
349
+
350
+ // Scenario 2: self-sent message containing a REF code (operator uses same device)
351
+ if (payload.is_sender) {
352
+ // Verify this REF code exists and is pending or recently responded (for follow-up documents)
353
+ const pendingRequest = await OperatorRequest.findOne({
354
+ ref_code: refMatch[1],
355
+ tenant_id: tenant._id,
356
+ status: { $in: ['pending', 'responded'] },
357
+ });
358
+ if (pendingRequest) {
359
+ logger.info('Operator message detected (self-sent)', { refCode: refMatch[1], message: payload.message.substring(0, 100) });
360
+ await handleOperatorReply(tenant, payload);
361
+ return;
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ // Determine contact info from the other party (attendees[0] is always the contact)
368
+ const contact = payload.attendees?.[0];
369
+ const contactName = contact?.attendee_name || 'Bilinmiyor';
370
+ const contactPhone = contact?.attendee_specifics?.phone_number
371
+ || contact?.attendee_provider_id || '';
372
+
373
+ // Filter out group chats — only process valid phone numbers
374
+ if (!isValidPhoneNumber(contactPhone)) {
375
+ logger.debug('Ignoring non-phone chat (group?)', { chat_id: payload.chat_id, contactPhone });
376
+ return;
377
+ }
378
+
379
+ // Upsert contact profile
380
+ const profile = await WhatsAppContactProfile.findOneAndUpdate(
381
+ { tenant_id: tenant._id, phone: contactPhone },
382
+ {
383
+ $set: { display_name: contactName, last_seen_at: new Date() },
384
+ $setOnInsert: { first_seen_at: new Date(), notes: '', tags: [], ai_summary: '', metadata: {} },
385
+ },
386
+ { upsert: true, new: true },
387
+ );
388
+
389
+ const gracePeriodSeconds = tenant.settings?.whatsapp_agent?.grace_period_seconds ?? 180;
390
+ const agentObjId = tenant.settings?.whatsapp_agent?.agent_id;
391
+ const agentDoc = agentObjId ? await Agent.findById(agentObjId).lean() : null;
392
+ const agentId = (agentDoc && agentDoc.is_active) ? agentDoc.elevenlabs_agent_id : '';
393
+ const blacklist = tenant.settings?.whatsapp_agent?.blacklisted_numbers || [];
394
+ const isBlacklisted = blacklist.includes(contactPhone);
395
+ const skipAI = isBlacklisted || isSystemMessage || isLegacyMediaPlaceholder;
396
+
397
+ // Find or create WhatsAppChat
398
+ let chat = await WhatsAppChat.findOne({ unipile_chat_id: payload.chat_id });
399
+
400
+ // If not found by chat_id, try matching by phone (handles provider migration)
401
+ if (!chat && contactPhone) {
402
+ chat = await WhatsAppChat.findOne({ tenant_id: tenant._id, contact_phone: contactPhone, deleted_at: null });
403
+ if (chat) {
404
+ const oldChatId = chat.unipile_chat_id;
405
+ chat.unipile_chat_id = payload.chat_id;
406
+ chat.unipile_account_id = payload.account_id;
407
+
408
+ // Clear stale session from previous provider
409
+ if (chat.active_session_id) {
410
+ const staleSession = await WhatsAppSession.findById(chat.active_session_id);
411
+ if (staleSession && staleSession.status !== 'resolved') {
412
+ staleSession.status = 'resolved';
413
+ staleSession.resolved_at = new Date();
414
+ await staleSession.save();
415
+ }
416
+ chat.active_session_id = null;
417
+ }
418
+
419
+ await chat.save();
420
+ logger.info('Relinked existing chat after provider migration', {
421
+ chatId: chat._id, oldChatId, newChatId: payload.chat_id,
422
+ });
423
+ }
424
+ }
425
+
426
+ // ─── New chat ──────────────────────────────────────────────
427
+ if (!chat) {
428
+ chat = await WhatsAppChat.create({
429
+ tenant_id: tenant._id,
430
+ unipile_chat_id: payload.chat_id,
431
+ unipile_account_id: payload.account_id,
432
+ contact_name: contactName,
433
+ contact_phone: contactPhone,
434
+ contact_profile_id: profile._id,
435
+ is_closed: false,
436
+ last_message_at: new Date(payload.timestamp),
437
+ last_message_preview: payload.message.substring(0, 100),
438
+ message_count: 0,
439
+ });
440
+
441
+ // Self-sent first message — our user initiated the chat
442
+ if (payload.is_sender) {
443
+ const session = await WhatsAppSession.create({
444
+ chat_id: chat._id,
445
+ tenant_id: tenant._id,
446
+ status: 'active',
447
+ started_at: new Date(),
448
+ taken_over_at: new Date(),
449
+ });
450
+ chat.active_session_id = session._id;
451
+ await chat.save();
452
+
453
+ await WhatsAppMessage.create({
454
+ chat_id: chat._id,
455
+ session_id: session._id,
456
+ tenant_id: tenant._id,
457
+ unipile_message_id: payload.message_id || null,
458
+ sender: 'human',
459
+ sender_name: 'WhatsApp',
460
+ text: payload.message,
461
+ sent_via_unipile: false,
462
+ });
463
+ chat.message_count = 1;
464
+ session.message_count = 1;
465
+ await Promise.all([chat.save(), session.save()]);
466
+ logger.info('Created new chat from self-sent message (human mode)', {
467
+ chatId: chat._id,
468
+ sessionId: session._id,
469
+ contact: contactName,
470
+ });
471
+ return;
472
+ }
473
+
474
+ if (skipAI) {
475
+ await WhatsAppMessage.create({
476
+ chat_id: chat._id,
477
+ session_id: null,
478
+ tenant_id: tenant._id,
479
+ unipile_message_id: payload.message_id || null,
480
+ sender: 'contact',
481
+ sender_name: contactName,
482
+ text: payload.message,
483
+ media_type: payload.content_type || null,
484
+ ad_context: payload.ad_context || null,
485
+ sent_via_unipile: false,
486
+ });
487
+ chat.message_count = 1;
488
+ await chat.save();
489
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
490
+ logger.info('Created new chat — AI skipped', {
491
+ chatId: chat._id,
492
+ contact: contactName,
493
+ reason: isSystemMessage ? 'system_message' : 'blacklisted',
494
+ });
495
+ return;
496
+ }
497
+
498
+ const session = await createSession(chat, tenant._id.toString(), gracePeriodSeconds);
499
+
500
+ await WhatsAppMessage.create({
501
+ chat_id: chat._id,
502
+ session_id: session._id,
503
+ tenant_id: tenant._id,
504
+ unipile_message_id: payload.message_id || null,
505
+ sender: 'contact',
506
+ sender_name: contactName,
507
+ text: payload.message,
508
+ media_type: payload.content_type || null,
509
+ ad_context: payload.ad_context || null,
510
+ sent_via_unipile: false,
511
+ });
512
+ chat.message_count = 1;
513
+ await chat.save();
514
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
515
+
516
+ session.message_count = 1;
517
+ await session.save();
518
+
519
+ logger.info('Created new WhatsApp chat with session', {
520
+ chatId: chat._id,
521
+ sessionId: session._id,
522
+ contact: contactName,
523
+ gracePeriodSeconds,
524
+ });
525
+
526
+ // Send fixed first message if enabled
527
+ const fixedMsg = tenant.settings?.whatsapp_agent?.fixed_first_message;
528
+ if (tenant.settings?.whatsapp_agent?.fixed_first_message_enabled && fixedMsg) {
529
+ const provider = unipileService.getProviderForTenant(tenant);
530
+ const chatRef = chat;
531
+ unipileService.sendMessage(provider, chatRef.unipile_chat_id, fixedMsg).then(async (result) => {
532
+ await WhatsAppMessage.create({
533
+ chat_id: chatRef._id,
534
+ session_id: session._id,
535
+ tenant_id: tenant._id,
536
+ unipile_message_id: result.message_id || null,
537
+ sender: 'system',
538
+ sender_name: 'AI',
539
+ text: fixedMsg,
540
+ sent_via_unipile: true,
541
+ });
542
+ chatRef.message_count += 1;
543
+ await chatRef.save();
544
+ }).catch(err => {
545
+ logger.error('Failed to send fixed first message', { chatId: chatRef._id, error: (err as Error).message });
546
+ });
547
+ }
548
+
549
+ await whatsappAgentService.startSession({
550
+ session,
551
+ gracePeriodSeconds,
552
+ agentId,
553
+ tenantId: tenant._id.toString(),
554
+ });
555
+ return;
556
+ }
557
+
558
+ // Backfill profile link on existing chats
559
+ if (!chat.contact_profile_id) {
560
+ chat.contact_profile_id = profile._id;
561
+ await chat.save();
562
+ }
563
+
564
+ // Update contact name if changed
565
+ if (chat.contact_name !== contactName && contactName !== 'Bilinmiyor') {
566
+ chat.contact_name = contactName;
567
+ await chat.save();
568
+ }
569
+
570
+ // ─── Self-sent messages (is_sender=true) ───────────────────
571
+ if (payload.is_sender) {
572
+ // Look up active session
573
+ let session = chat.active_session_id
574
+ ? await WhatsAppSession.findById(chat.active_session_id)
575
+ : null;
576
+ if (session && session.status === 'resolved') session = null;
577
+
578
+ // If session is waiting, human responded — cancel timer & resolve
579
+ let resolvedSessionId: mongoose.Types.ObjectId | null = null;
580
+ if (session && session.status === 'waiting') {
581
+ resolvedSessionId = session._id;
582
+ await whatsappAgentService.resolveSessionAsHuman(session);
583
+ session = null; // session is now resolved
584
+ }
585
+
586
+ // Check if we already stored this message (sent via our API)
587
+ // Primary dedup: by unipile_message_id (if Unipile returned one when we sent)
588
+ // Fallback dedup: by text + sent_via_unipile + recent timestamp (covers cases
589
+ // where Unipile didn't return a message_id in the send response)
590
+ if (payload.message_id) {
591
+ const exists = await WhatsAppMessage.findOne({ unipile_message_id: payload.message_id });
592
+ if (exists) {
593
+ logger.debug('Self-sent message already in DB (by message_id), skipping', { message_id: payload.message_id });
594
+ return;
595
+ }
596
+ }
597
+ const recentCutoff = new Date(Date.now() - 60_000); // 1 minute window
598
+ const existsByContent = await WhatsAppMessage.findOne({
599
+ chat_id: chat._id,
600
+ sent_via_unipile: true,
601
+ text: payload.message,
602
+ createdAt: { $gte: recentCutoff },
603
+ });
604
+ if (existsByContent) {
605
+ // Update the stored message with the real unipile_message_id for future dedup
606
+ if (payload.message_id && !existsByContent.unipile_message_id) {
607
+ await WhatsAppMessage.updateOne(
608
+ { _id: existsByContent._id },
609
+ { unipile_message_id: payload.message_id },
610
+ );
611
+ }
612
+ logger.debug('Self-sent message already in DB (by content), skipping', { message_id: payload.message_id });
613
+ return;
614
+ }
615
+
616
+ // Store the message — link to the session it resolved (if any) or the current active session
617
+ await WhatsAppMessage.create({
618
+ chat_id: chat._id,
619
+ session_id: resolvedSessionId || session?._id || null,
620
+ tenant_id: tenant._id,
621
+ unipile_message_id: payload.message_id,
622
+ sender: 'human',
623
+ sender_name: 'WhatsApp',
624
+ text: payload.message,
625
+ sent_via_unipile: false,
626
+ });
627
+
628
+ chat.last_message_at = new Date(payload.timestamp);
629
+ chat.last_message_preview = payload.message.substring(0, 100);
630
+ chat.message_count += 1;
631
+ await chat.save();
632
+
633
+ // If session is active with no takeover (AI mode), switch to human
634
+ if (session && session.status === 'active' && !session.taken_over_by && !session.taken_over_at) {
635
+ session.taken_over_at = new Date();
636
+ await session.save();
637
+ whatsappAgentService.closeConnection(chat.unipile_chat_id);
638
+ logger.info('Auto-switched to human mode (manual WhatsApp reply detected)', {
639
+ chatId: chat._id,
640
+ sessionId: session._id,
641
+ });
642
+ }
643
+
644
+ logger.info('Stored self-sent message from WhatsApp', { chatId: chat._id });
645
+ return;
646
+ }
647
+
648
+ // ─── Incoming contact message (is_sender=false) ────────────
649
+
650
+ // Blacklisted numbers & system messages: store but never trigger session or AI
651
+ if (skipAI) {
652
+ await WhatsAppMessage.create({
653
+ chat_id: chat._id,
654
+ session_id: null,
655
+ tenant_id: tenant._id,
656
+ unipile_message_id: payload.message_id || null,
657
+ sender: 'contact',
658
+ sender_name: contactName,
659
+ text: payload.message,
660
+ media_type: payload.content_type || null,
661
+ ad_context: payload.ad_context || null,
662
+ sent_via_unipile: false,
663
+ });
664
+ chat.last_message_at = new Date(payload.timestamp);
665
+ chat.last_message_preview = payload.message.substring(0, 100);
666
+ chat.message_count += 1;
667
+ if (chat.is_closed) chat.is_closed = false;
668
+ if (chat.deleted_at) chat.deleted_at = null;
669
+ await chat.save();
670
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
671
+ logger.info('Message stored — AI skipped', {
672
+ chatId: chat._id,
673
+ reason: isSystemMessage ? 'system_message' : 'blacklisted',
674
+ });
675
+ return;
676
+ }
677
+
678
+ // Look up active session
679
+ let session = chat.active_session_id
680
+ ? await WhatsAppSession.findById(chat.active_session_id)
681
+ : null;
682
+ if (session && session.status === 'resolved') {
683
+ session = null;
684
+ chat.active_session_id = null;
685
+ await chat.save();
686
+ }
687
+
688
+ // If session is waiting, just store the message (timer already running)
689
+ if (session && session.status === 'waiting') {
690
+ await WhatsAppMessage.create({
691
+ chat_id: chat._id,
692
+ session_id: session._id,
693
+ tenant_id: tenant._id,
694
+ unipile_message_id: payload.message_id || null,
695
+ sender: 'contact',
696
+ sender_name: contactName,
697
+ text: payload.message,
698
+ media_type: payload.content_type || null,
699
+ ad_context: payload.ad_context || null,
700
+ sent_via_unipile: false,
701
+ });
702
+ chat.last_message_at = new Date(payload.timestamp);
703
+ chat.last_message_preview = payload.message.substring(0, 100);
704
+ chat.message_count += 1;
705
+ await chat.save();
706
+ session.message_count += 1;
707
+ await session.save();
708
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
709
+ logger.info('Message stored during grace period', { chatId: chat._id, sessionId: session._id });
710
+ return;
711
+ }
712
+
713
+ // If session is active, forward to agent service
714
+ if (session && session.status === 'active') {
715
+ await whatsappAgentService.handleIncomingMessage({
716
+ tenantId: tenant._id.toString(),
717
+ agentId,
718
+ chat,
719
+ session,
720
+ messageText: payload.message,
721
+ senderName: contactName,
722
+ unipileMessageId: payload.message_id,
723
+ contentType: payload.content_type,
724
+ });
725
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
726
+ return;
727
+ }
728
+
729
+ // No active session → create new one (closed or idle chat)
730
+ session = await createSession(chat, tenant._id.toString(), gracePeriodSeconds);
731
+
732
+ await WhatsAppMessage.create({
733
+ chat_id: chat._id,
734
+ session_id: session._id,
735
+ tenant_id: tenant._id,
736
+ unipile_message_id: payload.message_id || null,
737
+ sender: 'contact',
738
+ sender_name: contactName,
739
+ text: payload.message,
740
+ media_type: payload.content_type || null,
741
+ ad_context: payload.ad_context || null,
742
+ sent_via_unipile: false,
743
+ });
744
+ chat.last_message_at = new Date(payload.timestamp);
745
+ chat.last_message_preview = payload.message.substring(0, 100);
746
+ chat.message_count += 1;
747
+ await chat.save();
748
+ await WhatsAppContactProfile.updateOne({ _id: profile._id }, { $inc: { total_messages: 1 } });
749
+
750
+ session.message_count = 1;
751
+ await session.save();
752
+
753
+ await whatsappAgentService.startSession({
754
+ session,
755
+ gracePeriodSeconds,
756
+ agentId,
757
+ tenantId: tenant._id.toString(),
758
+ });
759
+
760
+ logger.info('Started new session for chat', {
761
+ chatId: chat._id,
762
+ sessionId: session._id,
763
+ });
764
+ }
765
+
766
+ // ─── Operator Workflow Helpers ──────────────────────────────
767
+
768
+ /**
769
+ * Handle a reply from the operator (doctor) that contains a REF code.
770
+ * Matches the reply to the correct patient session and injects the response.
771
+ */
772
+ async function handleOperatorReply(
773
+ tenant: InstanceType<typeof Tenant>,
774
+ payload: UnipileWebhookPayload,
775
+ ): Promise<void> {
776
+ const messageText = payload.message.trim();
777
+ const provider = unipileService.getProviderForTenant(tenant);
778
+
779
+ // Extract REF code from the message text or quoted (replied-to) message
780
+ const refMatch = messageText.match(/\b([A-Z]-[A-Z0-9]{4})\b/)
781
+ || (payload.quoted_text?.match(/\b([A-Z]-[A-Z0-9]{4})\b/) ?? null);
782
+ if (!refMatch) {
783
+ logger.info('Operator message without REF code — storing as normal', {
784
+ message: messageText.substring(0, 100),
785
+ });
786
+ // No REF code — this is just a normal message from the doctor, ignore it
787
+ // (could also store it as a chat message, but the doctor is messaging the clinic number)
788
+ return;
789
+ }
790
+
791
+ const refCode = refMatch[1];
792
+ // Match pending OR recently responded (estetisyen may send document after text reply)
793
+ const request = await OperatorRequest.findOne({
794
+ ref_code: refCode,
795
+ tenant_id: tenant._id,
796
+ status: { $in: ['pending', 'responded'] },
797
+ }).sort({ createdAt: -1 });
798
+
799
+ if (!request) {
800
+ logger.warn('No matching OperatorRequest for REF code', { refCode, tenantId: tenant._id });
801
+ return;
802
+ }
803
+
804
+ // Check if operator sent media (document/image) along with the reply
805
+ const hasMedia = !!payload.content_type && payload.content_type !== 'conversation' && payload.content_type !== 'extendedTextMessage';
806
+
807
+ logger.info('Operator reply media check', {
808
+ refCode,
809
+ content_type: payload.content_type,
810
+ message_id: payload.message_id,
811
+ chat_id: payload.chat_id,
812
+ hasMedia,
813
+ is_sender: payload.is_sender,
814
+ });
815
+
816
+ const alreadyResponded = request.status === 'responded';
817
+
818
+ // Update the operator request
819
+ request.status = 'responded';
820
+ if (!alreadyResponded) {
821
+ request.operator_response = messageText;
822
+ request.responded_at = new Date();
823
+ }
824
+ if (hasMedia) {
825
+ request.response_media_type = payload.content_type!;
826
+ request.response_message_id = payload.message_id;
827
+ }
828
+ await request.save();
829
+
830
+ // If operator sent media, forward it directly to the patient
831
+ let mediaForwarded = false;
832
+ if (hasMedia) {
833
+ const chat = await WhatsAppChat.findById(request.chat_id).lean();
834
+ if (chat) {
835
+ try {
836
+ // Small delay to ensure Baileys worker has cached the media
837
+ await new Promise(resolve => setTimeout(resolve, 1000));
838
+ await unipileService.forwardMedia(
839
+ provider,
840
+ payload.chat_id,
841
+ payload.message_id,
842
+ chat.contact_phone,
843
+ '',
844
+ );
845
+ mediaForwarded = true;
846
+ logger.info('Forwarded operator document to patient', {
847
+ refCode, mediaType: payload.content_type, patientPhone: chat.contact_phone,
848
+ });
849
+ } catch (err) {
850
+ logger.error('Failed to forward operator document to patient', {
851
+ refCode, error: (err as Error).message,
852
+ });
853
+ }
854
+ }
855
+ }
856
+
857
+ if (!alreadyResponded) {
858
+ // First reply: inject the operator's text response into the patient's AI session
859
+ await whatsappAgentService.injectOperatorResponse({
860
+ tenantId: tenant._id.toString(),
861
+ sessionId: request.session_id.toString(),
862
+ chatId: request.chat_id.toString(),
863
+ operatorResponse: messageText,
864
+ refCode,
865
+ hasDocument: mediaForwarded,
866
+ });
867
+
868
+ // Send confirmation to the estetisyen
869
+ try {
870
+ const accountId = tenant.settings.whatsapp_agent.unipile_account_id;
871
+ if (accountId) {
872
+ const operatorPhone = tenant.settings.whatsapp_agent.operator_phone!;
873
+ const operatorJid = operatorPhone.replace(/[^0-9]/g, '') + '@s.whatsapp.net';
874
+ const operatorChatId = `${accountId}:${operatorJid}`;
875
+ const confirmMsg = mediaForwarded
876
+ ? `✓ Yanıt ve belge hastaya iletildi. Ref: ${refCode}`
877
+ : `✓ Yanıt alındı. Ref: ${refCode} — Hastaya iletilecek.`;
878
+ await unipileService.sendMessage(provider, operatorChatId, confirmMsg);
879
+ }
880
+ } catch (err) {
881
+ logger.error('Failed to send confirmation to operator', { refCode, error: (err as Error).message });
882
+ }
883
+ } else if (mediaForwarded) {
884
+ // Follow-up: estetisyen sent document after initial text reply
885
+ // Notify AI about the document so it can tell the patient
886
+ await whatsappAgentService.injectOperatorResponse({
887
+ tenantId: tenant._id.toString(),
888
+ sessionId: request.session_id.toString(),
889
+ chatId: request.chat_id.toString(),
890
+ operatorResponse: '',
891
+ refCode,
892
+ hasDocument: true,
893
+ });
894
+
895
+ // Send confirmation
896
+ try {
897
+ const accountId = tenant.settings.whatsapp_agent.unipile_account_id;
898
+ if (accountId) {
899
+ const operatorPhone = tenant.settings.whatsapp_agent.operator_phone!;
900
+ const operatorJid = operatorPhone.replace(/[^0-9]/g, '') + '@s.whatsapp.net';
901
+ const operatorChatId = `${accountId}:${operatorJid}`;
902
+ await unipileService.sendMessage(provider, operatorChatId, `✓ Belge hastaya iletildi. Ref: ${refCode}`);
903
+ }
904
+ } catch (err) {
905
+ logger.error('Failed to send confirmation to operator', { refCode, error: (err as Error).message });
906
+ }
907
+ }
908
+
909
+ logger.info('Operator reply processed', {
910
+ refCode,
911
+ sessionId: request.session_id,
912
+ hasMedia,
913
+ mediaForwarded,
914
+ alreadyResponded,
915
+ response: messageText.substring(0, 100),
916
+ });
917
+ }