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,1966 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.default = superAdminRoutes;
40
+ const mongoose_1 = __importDefault(require("mongoose"));
41
+ const tenant_model_1 = __importStar(require("../tenants/tenant.model"));
42
+ const auth_model_1 = __importDefault(require("../auth/auth.model"));
43
+ const agent_model_1 = __importDefault(require("../agents/agent.model"));
44
+ const call_model_1 = __importDefault(require("../calls/call.model"));
45
+ const campaign_model_1 = __importDefault(require("../campaigns/campaign.model"));
46
+ const billing_model_1 = __importDefault(require("../billing/billing.model"));
47
+ const rbac_1 = require("../../plugins/rbac");
48
+ const auth_service_1 = require("../auth/auth.service");
49
+ const plan_model_1 = __importDefault(require("../plans/plan.model"));
50
+ const payment_model_1 = __importDefault(require("../billing/payment.model"));
51
+ const elevenlabs_service_1 = __importDefault(require("../../services/elevenlabs.service"));
52
+ const billing_service_1 = require("../billing/billing.service");
53
+ const logger_1 = __importDefault(require("../../utils/logger"));
54
+ const elevenlabs_test_chat_routes_1 = __importDefault(require("./elevenlabs-test-chat.routes"));
55
+ const whatsapp_test_chat_routes_1 = __importDefault(require("./whatsapp-test-chat.routes"));
56
+ const migration_routes_1 = __importDefault(require("../../migrations/migration.routes"));
57
+ const whatsapp_session_model_1 = __importDefault(require("../whatsapp/whatsapp-session.model"));
58
+ const whatsapp_message_model_1 = __importDefault(require("../whatsapp/whatsapp-message.model"));
59
+ const whatsapp_chat_model_1 = __importDefault(require("../whatsapp/whatsapp-chat.model"));
60
+ const whatsapp_contact_profile_model_1 = __importDefault(require("../whatsapp/whatsapp-contact-profile.model"));
61
+ const whatsapp_agent_service_1 = __importDefault(require("../../services/whatsapp-agent.service"));
62
+ const lead_model_1 = __importDefault(require("../leads/lead.model"));
63
+ const ConversationEvaluation_1 = __importDefault(require("../../models/ConversationEvaluation"));
64
+ const ConversationClaim_1 = __importDefault(require("../../models/ConversationClaim"));
65
+ const stream_1 = require("stream");
66
+ async function superAdminRoutes(fastify) {
67
+ fastify.addHook('preHandler', fastify.authenticate);
68
+ fastify.addHook('preHandler', (0, rbac_1.requireRole)('superadmin'));
69
+ // Register sub-routes
70
+ await fastify.register(elevenlabs_test_chat_routes_1.default, { prefix: '/elevenlabs/test-chat' });
71
+ await fastify.register(whatsapp_test_chat_routes_1.default, { prefix: '/whatsapp/test-chat' });
72
+ await fastify.register(migration_routes_1.default, { prefix: '/migrations' });
73
+ // ─── Tenants ────────────────────────────────────────────────
74
+ // GET /admin/tenants — list all tenants with user count
75
+ fastify.get('/tenants', async (request) => {
76
+ const page = parseInt(request.query.page || '1', 10);
77
+ const limit = 20;
78
+ const skip = (page - 1) * limit;
79
+ const filter = { $or: [{ is_active: true }, { is_active: { $exists: false } }] };
80
+ if (request.query.search) {
81
+ filter.name = { $regex: request.query.search, $options: 'i' };
82
+ }
83
+ const [tenants, total] = await Promise.all([
84
+ tenant_model_1.default.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
85
+ tenant_model_1.default.countDocuments(filter)
86
+ ]);
87
+ // Attach user count per tenant
88
+ const tenantIds = tenants.map(t => t._id);
89
+ const userCounts = await auth_model_1.default.aggregate([
90
+ { $match: { tenant_id: { $in: tenantIds } } },
91
+ { $group: { _id: '$tenant_id', count: { $sum: 1 } } }
92
+ ]);
93
+ const userCountMap = new Map(userCounts.map(u => [String(u._id), u.count]));
94
+ const enriched = tenants.map(t => ({
95
+ ...t,
96
+ user_count: userCountMap.get(String(t._id)) || 0
97
+ }));
98
+ return { tenants: enriched, total, page, totalPages: Math.ceil(total / limit) };
99
+ });
100
+ // POST /admin/tenants — create tenant manually
101
+ fastify.post('/tenants', async (request, reply) => {
102
+ const { name, plan, enabled_features } = request.body;
103
+ if (!name) {
104
+ return reply.status(400).send({ error: 'name is required' });
105
+ }
106
+ const tenant = await tenant_model_1.default.create({
107
+ name,
108
+ plan: plan || 'free',
109
+ enabled_features: enabled_features || [],
110
+ settings: { kvkk_consent: false, notification_recipients: [], default_language: 'tr' }
111
+ });
112
+ return reply.status(201).send({ tenant });
113
+ });
114
+ // GET /admin/tenants/:id — full tenant detail
115
+ fastify.get('/tenants/:id', async (request, reply) => {
116
+ const { id } = request.params;
117
+ const tenant = await tenant_model_1.default.findById(id).lean();
118
+ if (!tenant) {
119
+ return reply.status(404).send({ error: 'Tenant not found' });
120
+ }
121
+ const [users, agents, callCount, campaignCount] = await Promise.all([
122
+ auth_model_1.default.find({ tenant_id: id }).select('name email role createdAt').lean(),
123
+ agent_model_1.default.find({ tenant_id: id }).lean(),
124
+ call_model_1.default.countDocuments({ tenant_id: id }),
125
+ campaign_model_1.default.countDocuments({ tenant_id: id })
126
+ ]);
127
+ return { tenant, users, agents, callCount, campaignCount };
128
+ });
129
+ // PUT /admin/tenants/:id — update tenant
130
+ fastify.put('/tenants/:id', async (request, reply) => {
131
+ const { id } = request.params;
132
+ const { name, plan, enabled_features, settings } = request.body;
133
+ const tenant = await tenant_model_1.default.findById(id);
134
+ if (!tenant) {
135
+ return reply.status(404).send({ error: 'Tenant not found' });
136
+ }
137
+ if (name)
138
+ tenant.name = name;
139
+ if (plan)
140
+ tenant.plan = plan;
141
+ if (enabled_features) {
142
+ tenant.enabled_features = enabled_features.filter(f => tenant_model_1.AVAILABLE_FEATURES.includes(f));
143
+ }
144
+ if (settings) {
145
+ const { whatsapp_agent, ...rest } = settings;
146
+ Object.assign(tenant.settings, rest);
147
+ if (whatsapp_agent && typeof whatsapp_agent === 'object') {
148
+ Object.assign(tenant.settings.whatsapp_agent, whatsapp_agent);
149
+ }
150
+ }
151
+ await tenant.save();
152
+ return { tenant };
153
+ });
154
+ // PUT /admin/tenants/:id/features — toggle features
155
+ fastify.put('/tenants/:id/features', async (request, reply) => {
156
+ const { id } = request.params;
157
+ const { enabled_features } = request.body;
158
+ const tenant = await tenant_model_1.default.findById(id);
159
+ if (!tenant) {
160
+ return reply.status(404).send({ error: 'Tenant not found' });
161
+ }
162
+ tenant.enabled_features = enabled_features.filter(f => tenant_model_1.AVAILABLE_FEATURES.includes(f));
163
+ await tenant.save();
164
+ return { tenant };
165
+ });
166
+ // PUT /admin/tenants/:id/deactivate — soft delete (deactivate tenant)
167
+ fastify.put('/tenants/:id/deactivate', async (request, reply) => {
168
+ const { id } = request.params;
169
+ const tenant = await tenant_model_1.default.findById(id);
170
+ if (!tenant)
171
+ return reply.status(404).send({ error: 'Tenant not found' });
172
+ tenant.is_active = false;
173
+ tenant.deleted_at = new Date();
174
+ await tenant.save();
175
+ logger_1.default.info('Tenant deactivated (soft delete)', { tenantId: id, name: tenant.name });
176
+ return { tenant };
177
+ });
178
+ // PUT /admin/tenants/:id/reactivate — undo soft delete
179
+ fastify.put('/tenants/:id/reactivate', async (request, reply) => {
180
+ const { id } = request.params;
181
+ const tenant = await tenant_model_1.default.findById(id);
182
+ if (!tenant)
183
+ return reply.status(404).send({ error: 'Tenant not found' });
184
+ tenant.is_active = true;
185
+ tenant.deleted_at = null;
186
+ await tenant.save();
187
+ logger_1.default.info('Tenant reactivated', { tenantId: id, name: tenant.name });
188
+ return { tenant };
189
+ });
190
+ // DELETE /admin/tenants/:id — hard delete (tombstones data, mangles unique fields)
191
+ fastify.delete('/tenants/:id', async (request, reply) => {
192
+ const { id } = request.params;
193
+ const tenant = await tenant_model_1.default.findById(id);
194
+ if (!tenant)
195
+ return reply.status(404).send({ error: 'Tenant not found' });
196
+ if (tenant.is_active) {
197
+ return reply.status(400).send({ error: 'Tenant must be deactivated before permanent deletion' });
198
+ }
199
+ const suffix = `___deleted_${Date.now()}`;
200
+ const tenantOid = new mongoose_1.default.Types.ObjectId(id);
201
+ // Mangle unique fields to avoid conflicts, mark as deleted
202
+ await Promise.all([
203
+ // Mangle user emails so they can re-register
204
+ auth_model_1.default.updateMany({ tenant_id: tenantOid }, [{ $set: { email: { $concat: ['$email', suffix] }, is_active: false } }]),
205
+ // Deactivate agents
206
+ agent_model_1.default.updateMany({ tenant_id: tenantOid }, { $set: { is_active: false } }),
207
+ // Mangle tenant name
208
+ tenant_model_1.default.updateOne({ _id: tenantOid }, { $set: { name: `${tenant.name}${suffix}`, is_active: false, deleted_at: new Date() } }),
209
+ ]);
210
+ logger_1.default.info('Tenant hard deleted (tombstoned)', { tenantId: id, name: tenant.name, suffix });
211
+ return { deleted: true };
212
+ });
213
+ // ─── User Creation for Tenant ──────────────────────────────
214
+ // POST /admin/tenants/:id/users — create user for a tenant
215
+ fastify.post('/tenants/:id/users', async (request, reply) => {
216
+ const { id } = request.params;
217
+ const { email, password, name, role } = request.body;
218
+ if (!email || !password || !name) {
219
+ return reply.status(400).send({ error: 'email, password, and name are required' });
220
+ }
221
+ const tenant = await tenant_model_1.default.findById(id);
222
+ if (!tenant) {
223
+ return reply.status(404).send({ error: 'Tenant not found' });
224
+ }
225
+ const existing = await auth_model_1.default.findOne({ email: email.toLowerCase() });
226
+ if (existing) {
227
+ return reply.status(409).send({ error: 'Bu e-posta zaten kullaniliyor' });
228
+ }
229
+ const password_hash = await (0, auth_service_1.hashPassword)(password);
230
+ const user = await auth_model_1.default.create({
231
+ tenant_id: id,
232
+ email: email.toLowerCase(),
233
+ password_hash,
234
+ name,
235
+ role: role || 'admin'
236
+ });
237
+ return reply.status(201).send({
238
+ user: { _id: user._id, name: user.name, email: user.email, role: user.role, createdAt: user.createdAt }
239
+ });
240
+ });
241
+ // ─── Agent Provisioning ─────────────────────────────────────
242
+ // GET /admin/elevenlabs/available-agents — EL agents not yet assigned to any tenant
243
+ fastify.get('/elevenlabs/available-agents', async () => {
244
+ // Get all ElevenLabs agents
245
+ const elAgents = await elevenlabs_service_1.default.listAgents({ pageSize: 100 });
246
+ // Get all already-assigned ElevenLabs agent IDs
247
+ const assignedIds = await agent_model_1.default.distinct('elevenlabs_agent_id');
248
+ const assignedSet = new Set(assignedIds);
249
+ const available = elAgents.agents
250
+ .filter(a => !assignedSet.has(a.agentId))
251
+ .map(a => ({
252
+ agent_id: a.agentId,
253
+ name: a.name,
254
+ }));
255
+ return { agents: available };
256
+ });
257
+ // POST /admin/agents — provision agent for a tenant
258
+ fastify.post('/agents', async (request, reply) => {
259
+ const { tenant_id, phone_number, elevenlabs_agent_id, direction, post_call_processing } = request.body;
260
+ if (!tenant_id || !phone_number || !elevenlabs_agent_id) {
261
+ return reply.status(400).send({
262
+ error: 'tenant_id, phone_number, and elevenlabs_agent_id are required'
263
+ });
264
+ }
265
+ const tenant = await tenant_model_1.default.findById(tenant_id);
266
+ if (!tenant) {
267
+ return reply.status(404).send({ error: 'Tenant not found' });
268
+ }
269
+ // Prevent duplicate assignment within the same tenant
270
+ const existing = await agent_model_1.default.findOne({ elevenlabs_agent_id, tenant_id });
271
+ if (existing) {
272
+ return reply.status(409).send({ error: 'Bu ElevenLabs agent zaten bu tenanta atanmis' });
273
+ }
274
+ const agent = await agent_model_1.default.create({
275
+ tenant_id,
276
+ phone_number,
277
+ elevenlabs_agent_id,
278
+ direction: direction && ['inbound', 'outbound', 'both'].includes(direction) ? direction : 'inbound',
279
+ agent_template: 'custom',
280
+ config: {
281
+ voice_id: 'default',
282
+ language: 'tr',
283
+ greeting: '',
284
+ template_data: {}
285
+ },
286
+ post_call_processing: {
287
+ strategy: 'summarize',
288
+ llm_provider: post_call_processing?.llm_provider || 'openai',
289
+ whatsapp_notify: true,
290
+ notification_template: 'call_summary',
291
+ summary_prompt: post_call_processing?.summary_prompt || '',
292
+ system_message: '',
293
+ whatsapp_message_template: post_call_processing?.whatsapp_message_template || ''
294
+ },
295
+ is_active: true
296
+ });
297
+ return reply.status(201).send({ agent });
298
+ });
299
+ // PUT /admin/agents/:id — update agent (superadmin)
300
+ fastify.put('/agents/:id', async (request, reply) => {
301
+ const { id } = request.params;
302
+ const { phone_number, direction, is_active, post_call_processing } = request.body;
303
+ const agent = await agent_model_1.default.findById(id);
304
+ if (!agent) {
305
+ return reply.status(404).send({ error: 'Agent not found' });
306
+ }
307
+ if (phone_number !== undefined)
308
+ agent.phone_number = phone_number;
309
+ if (direction && ['inbound', 'outbound', 'both'].includes(direction))
310
+ agent.direction = direction;
311
+ if (is_active !== undefined)
312
+ agent.is_active = is_active;
313
+ if (post_call_processing) {
314
+ if (post_call_processing.summary_prompt !== undefined) {
315
+ agent.post_call_processing.summary_prompt = post_call_processing.summary_prompt;
316
+ }
317
+ if (post_call_processing.whatsapp_message_template !== undefined) {
318
+ agent.post_call_processing.whatsapp_message_template = post_call_processing.whatsapp_message_template;
319
+ }
320
+ }
321
+ await agent.save();
322
+ return { agent };
323
+ });
324
+ // ─── System Health ──────────────────────────────────────────
325
+ // GET /admin/system/health
326
+ fastify.get('/system/health', async () => {
327
+ const now = new Date();
328
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
329
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
330
+ const sevenDaysAgo = new Date(now);
331
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
332
+ sevenDaysAgo.setHours(0, 0, 0, 0);
333
+ const [tenantCount, userCount, agentCount, totalCalls, todayCalls, activeCampaigns, monthlyRevenue, callStatusRows, dailyTrendRows] = await Promise.all([
334
+ tenant_model_1.default.countDocuments(),
335
+ auth_model_1.default.countDocuments(),
336
+ agent_model_1.default.countDocuments(),
337
+ call_model_1.default.countDocuments(),
338
+ call_model_1.default.countDocuments({ createdAt: { $gte: todayStart } }),
339
+ campaign_model_1.default.countDocuments({ status: { $in: ['running', 'scheduled'] } }),
340
+ billing_model_1.default.aggregate([
341
+ { $match: { type: 'call_charge', createdAt: { $gte: monthStart } } },
342
+ { $group: { _id: null, total: { $sum: '$amount' } } }
343
+ ]).then(r => r[0]?.total || 0),
344
+ // Call status breakdown this month
345
+ call_model_1.default.aggregate([
346
+ { $match: { createdAt: { $gte: monthStart } } },
347
+ { $group: { _id: '$status', count: { $sum: 1 } } }
348
+ ]),
349
+ // Daily trend last 7 days
350
+ call_model_1.default.aggregate([
351
+ { $match: { createdAt: { $gte: sevenDaysAgo } } },
352
+ { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, count: { $sum: 1 } } },
353
+ { $sort: { _id: 1 } }
354
+ ])
355
+ ]);
356
+ // Build status breakdown map
357
+ const callStatusBreakdown = { completed: 0, failed: 0, no_answer: 0, voicemail: 0, transferred: 0 };
358
+ callStatusRows.forEach((r) => { callStatusBreakdown[r._id] = r.count; });
359
+ // Fill 7-day trend with zeros for missing days
360
+ const dailyTrend = [];
361
+ for (let i = 6; i >= 0; i--) {
362
+ const d = new Date(now);
363
+ d.setDate(d.getDate() - i);
364
+ const key = d.toISOString().slice(0, 10);
365
+ const found = dailyTrendRows.find((r) => r._id === key);
366
+ dailyTrend.push({ date: key, count: found?.count || 0 });
367
+ }
368
+ return {
369
+ status: 'ok',
370
+ uptime: process.uptime(),
371
+ stats: {
372
+ tenants: tenantCount,
373
+ users: userCount,
374
+ agents: agentCount,
375
+ total_calls: totalCalls,
376
+ today_calls: todayCalls,
377
+ active_campaigns: activeCampaigns,
378
+ monthly_revenue: monthlyRevenue
379
+ },
380
+ call_status_breakdown: callStatusBreakdown,
381
+ daily_trend: dailyTrend,
382
+ system: {
383
+ memory: process.memoryUsage(),
384
+ node_version: process.version,
385
+ db_state: mongoose_1.default.connection.readyState
386
+ }
387
+ };
388
+ });
389
+ // GET /admin/system/usage — global usage per tenant with quota info
390
+ fastify.get('/system/usage', async () => {
391
+ const monthStart = new Date();
392
+ monthStart.setDate(1);
393
+ monthStart.setHours(0, 0, 0, 0);
394
+ const now = new Date();
395
+ const [usagePerTenant, allPlans, activeTopUps] = await Promise.all([
396
+ call_model_1.default.aggregate([
397
+ { $match: { createdAt: { $gte: monthStart } } },
398
+ {
399
+ $group: {
400
+ _id: '$tenant_id',
401
+ calls: { $sum: 1 },
402
+ duration: { $sum: '$duration_seconds' },
403
+ completed: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, 1, 0] } },
404
+ failed: { $sum: { $cond: [{ $eq: ['$status', 'failed'] }, 1, 0] } },
405
+ no_answer: { $sum: { $cond: [{ $eq: ['$status', 'no_answer'] }, 1, 0] } }
406
+ }
407
+ },
408
+ {
409
+ $lookup: {
410
+ from: 'tenants',
411
+ localField: '_id',
412
+ foreignField: '_id',
413
+ as: 'tenant'
414
+ }
415
+ },
416
+ { $unwind: '$tenant' },
417
+ {
418
+ $project: {
419
+ tenant_id: '$_id',
420
+ tenant_name: '$tenant.name',
421
+ plan: '$tenant.plan',
422
+ calls: 1,
423
+ duration: 1,
424
+ completed: 1,
425
+ failed: 1,
426
+ no_answer: 1
427
+ }
428
+ },
429
+ { $sort: { calls: -1 } }
430
+ ]),
431
+ plan_model_1.default.find().lean(),
432
+ payment_model_1.default.aggregate([
433
+ {
434
+ $match: {
435
+ type: 'top_up',
436
+ period_start: { $lte: now },
437
+ period_end: { $gte: monthStart }
438
+ }
439
+ },
440
+ { $group: { _id: '$tenant_id', extra_minutes: { $sum: '$extra_minutes' } } }
441
+ ])
442
+ ]);
443
+ const planMap = new Map(allPlans.map(p => [p.slug, p.included_minutes]));
444
+ const topUpMap = new Map(activeTopUps.map((t) => [String(t._id), t.extra_minutes]));
445
+ const enriched = usagePerTenant.map((u) => {
446
+ const planMinutes = planMap.get(u.plan) || 0;
447
+ const topUpMinutes = topUpMap.get(String(u.tenant_id)) || 0;
448
+ const totalQuota = planMinutes + topUpMinutes;
449
+ const usedMinutes = Math.ceil(u.duration / 60);
450
+ const pct = totalQuota > 0 ? Math.round((usedMinutes / totalQuota) * 100) : 0;
451
+ return {
452
+ ...u,
453
+ plan_minutes: planMinutes,
454
+ top_up_minutes: topUpMinutes,
455
+ total_quota: totalQuota,
456
+ used_minutes: usedMinutes,
457
+ quota_percentage: Math.min(pct, 100),
458
+ over_quota: usedMinutes > totalQuota
459
+ };
460
+ });
461
+ return { usage: enriched };
462
+ });
463
+ // GET /admin/templates — list agent templates
464
+ fastify.get('/templates', async () => {
465
+ return {
466
+ templates: [
467
+ { name: 'receptionist', direction: 'inbound', description: 'AI resepsiyonist — cagri karsilama, SSS, randevu' },
468
+ { name: 'survey', direction: 'outbound', description: 'AI anketci — yapilandirilmis soru-cevap' },
469
+ { name: 'custom', direction: 'both', description: 'Ozel prompt — tenant kendi promptunu yazar' }
470
+ ]
471
+ };
472
+ });
473
+ // ─── Plans ─────────────────────────────────────────────────
474
+ // GET /admin/plans — list all plans
475
+ fastify.get('/plans', async () => {
476
+ const plans = await plan_model_1.default.find().sort({ sort_order: 1 }).lean();
477
+ return { plans };
478
+ });
479
+ // POST /admin/plans — create plan
480
+ fastify.post('/plans', async (request, reply) => {
481
+ const { name, slug, price, included_minutes, overage_rate, language_support, sort_order, included_inbound_minutes, included_inbound_calls, included_outbound_minutes, included_outbound_calls, included_wa_chats, } = request.body;
482
+ if (!name || !slug || price == null || included_minutes == null || overage_rate == null) {
483
+ return reply.status(400).send({ error: 'name, slug, price, included_minutes, overage_rate are required' });
484
+ }
485
+ const existing = await plan_model_1.default.findOne({ slug });
486
+ if (existing) {
487
+ return reply.status(409).send({ error: 'Bu slug zaten kullaniliyor' });
488
+ }
489
+ const plan = await plan_model_1.default.create({
490
+ name, slug, price, included_minutes, overage_rate,
491
+ included_inbound_minutes: included_inbound_minutes ?? 0,
492
+ included_inbound_calls: included_inbound_calls ?? 0,
493
+ included_outbound_minutes: included_outbound_minutes ?? 0,
494
+ included_outbound_calls: included_outbound_calls ?? 0,
495
+ included_wa_chats: included_wa_chats ?? 0,
496
+ language_support: language_support || 'single',
497
+ sort_order: sort_order ?? 0
498
+ });
499
+ return reply.status(201).send({ plan });
500
+ });
501
+ // PUT /admin/plans/:id — update plan
502
+ fastify.put('/plans/:id', async (request, reply) => {
503
+ const { id } = request.params;
504
+ const updates = request.body;
505
+ const plan = await plan_model_1.default.findById(id);
506
+ if (!plan) {
507
+ return reply.status(404).send({ error: 'Plan not found' });
508
+ }
509
+ const allowed = ['name', 'slug', 'price', 'included_minutes', 'overage_rate', 'included_inbound_minutes', 'included_inbound_calls', 'included_outbound_minutes', 'included_outbound_calls', 'included_wa_chats', 'language_support', 'is_active', 'sort_order'];
510
+ for (const key of allowed) {
511
+ if (updates[key] !== undefined) {
512
+ plan[key] = updates[key];
513
+ }
514
+ }
515
+ await plan.save();
516
+ return { plan };
517
+ });
518
+ // POST /admin/plans/seed — seed default plans
519
+ fastify.post('/plans/seed', async (request, reply) => {
520
+ const count = await plan_model_1.default.countDocuments();
521
+ if (count > 0) {
522
+ return reply.status(409).send({ error: 'Planlar zaten mevcut', count });
523
+ }
524
+ const defaults = [
525
+ { name: 'Ucretsiz', slug: 'free', price: 0, included_minutes: 30, overage_rate: 0, language_support: 'single', sort_order: 0 },
526
+ { name: 'Mini Paket', slug: 'mini', price: 2999, included_minutes: 100, overage_rate: 34.99, language_support: 'single', sort_order: 1 },
527
+ { name: 'Standart Paket', slug: 'standart', price: 4999, included_minutes: 200, overage_rate: 29.99, language_support: 'single', sort_order: 2 },
528
+ { name: 'Pro Paket', slug: 'pro', price: 6999, included_minutes: 300, overage_rate: 24.99, language_support: 'dual', sort_order: 3 },
529
+ { name: 'Pro Max Paket', slug: 'pro-max', price: 9999, included_minutes: 500, overage_rate: 19.99, language_support: 'multi', sort_order: 4 },
530
+ ];
531
+ const plans = await plan_model_1.default.insertMany(defaults);
532
+ return reply.status(201).send({ plans });
533
+ });
534
+ // ─── Payments ───────────────────────────────────────────────
535
+ // GET /admin/payments — list all payments (filterable by tenant_id)
536
+ fastify.get('/payments', async (request) => {
537
+ const page = parseInt(request.query.page || '1', 10);
538
+ const limit = 20;
539
+ const skip = (page - 1) * limit;
540
+ const filter = {};
541
+ if (request.query.tenant_id) {
542
+ filter.tenant_id = request.query.tenant_id;
543
+ }
544
+ const [payments, total] = await Promise.all([
545
+ payment_model_1.default.find(filter).sort({ period_start: -1 }).skip(skip).limit(limit).lean(),
546
+ payment_model_1.default.countDocuments(filter)
547
+ ]);
548
+ return { payments, total, page, totalPages: Math.ceil(total / limit) };
549
+ });
550
+ // GET /admin/tenants/:id/payments — payments for a specific tenant
551
+ fastify.get('/tenants/:id/payments', async (request) => {
552
+ const { id } = request.params;
553
+ const payments = await payment_model_1.default.find({ tenant_id: id }).sort({ period_start: -1 }).lean();
554
+ return { payments };
555
+ });
556
+ // GET /admin/tenants/:id/billable-items — calls & AI WhatsApp sessions as billable objects
557
+ fastify.get('/tenants/:id/billable-items', async (request, reply) => {
558
+ const { id } = request.params;
559
+ const tenant = await tenant_model_1.default.findById(id).lean();
560
+ if (!tenant) {
561
+ return reply.status(404).send({ error: 'Tenant not found' });
562
+ }
563
+ const pageCalls = Math.max(1, parseInt(request.query.page_calls || '1', 10));
564
+ const pageChats = Math.max(1, parseInt(request.query.page_chats || request.query.page_sessions || '1', 10));
565
+ const limit = 20;
566
+ const skipCalls = (pageCalls - 1) * limit;
567
+ const skipChats = (pageChats - 1) * limit;
568
+ const now = new Date();
569
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
570
+ const from = request.query.from ? new Date(request.query.from) : monthStart;
571
+ const to = request.query.to ? new Date(request.query.to) : now;
572
+ if (isNaN(from.getTime()) || isNaN(to.getTime())) {
573
+ return reply.status(400).send({ error: 'Invalid date format for from/to' });
574
+ }
575
+ const tenantOid = new mongoose_1.default.Types.ObjectId(id);
576
+ const [callItems, callSummaryRows, sessionResult] = await Promise.all([
577
+ // Q1: Paginated calls
578
+ call_model_1.default.find({ tenant_id: id, createdAt: { $gte: from, $lte: to } })
579
+ .select('direction caller_number callee_number duration_seconds status cost createdAt agent_id conversation_id')
580
+ .sort({ createdAt: -1 })
581
+ .skip(skipCalls)
582
+ .limit(limit)
583
+ .lean(),
584
+ // Q2: Call summary (count + status breakdown + total duration + EL cost)
585
+ call_model_1.default.aggregate([
586
+ { $match: { tenant_id: tenantOid, createdAt: { $gte: from, $lte: to } } },
587
+ {
588
+ $group: {
589
+ _id: null,
590
+ total: { $sum: 1 },
591
+ total_duration: { $sum: '$duration_seconds' },
592
+ total_cost: { $sum: { $ifNull: ['$cost', 0] } },
593
+ completed: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, 1, 0] } },
594
+ failed: { $sum: { $cond: [{ $eq: ['$status', 'failed'] }, 1, 0] } },
595
+ no_answer: { $sum: { $cond: [{ $eq: ['$status', 'no_answer'] }, 1, 0] } },
596
+ voicemail: { $sum: { $cond: [{ $eq: ['$status', 'voicemail'] }, 1, 0] } },
597
+ transferred: { $sum: { $cond: [{ $eq: ['$status', 'transferred'] }, 1, 0] } },
598
+ },
599
+ },
600
+ ]),
601
+ // Q3: Billable WA chats (AI answered at least once in date range) + pagination
602
+ whatsapp_chat_model_1.default.aggregate([
603
+ { $match: { tenant_id: tenantOid } },
604
+ // Single messages lookup: count total, AI messages, and collect session_ids
605
+ {
606
+ $lookup: {
607
+ from: 'whatsappmessages',
608
+ let: { cid: '$_id' },
609
+ pipeline: [
610
+ { $match: { $expr: { $eq: ['$chat_id', '$$cid'] }, createdAt: { $gte: from, $lte: to } } },
611
+ {
612
+ $group: {
613
+ _id: null,
614
+ total: { $sum: 1 },
615
+ ai_count: { $sum: { $cond: [{ $eq: ['$sender_name', 'AI'] }, 1, 0] } },
616
+ session_ids: { $addToSet: '$session_id' },
617
+ },
618
+ },
619
+ ],
620
+ as: '_messages',
621
+ },
622
+ },
623
+ {
624
+ $addFields: {
625
+ ai_message_count: { $ifNull: [{ $arrayElemAt: ['$_messages.ai_count', 0] }, 0] },
626
+ period_message_count: { $ifNull: [{ $arrayElemAt: ['$_messages.total', 0] }, 0] },
627
+ _session_ids: { $ifNull: [{ $arrayElemAt: ['$_messages.session_ids', 0] }, []] },
628
+ },
629
+ },
630
+ { $match: { ai_message_count: { $gt: 0 } } },
631
+ // Lookup sessions by actual message activity (not started_at) for EL cost
632
+ {
633
+ $lookup: {
634
+ from: 'whatsappsessions',
635
+ let: { sids: '$_session_ids' },
636
+ pipeline: [
637
+ { $match: { $expr: { $in: ['$_id', '$$sids'] } } },
638
+ {
639
+ $group: {
640
+ _id: null,
641
+ count: { $sum: 1 },
642
+ total_cost: { $sum: { $ifNull: ['$elevenlabs_cost', 0] } },
643
+ },
644
+ },
645
+ ],
646
+ as: '_sessions',
647
+ },
648
+ },
649
+ {
650
+ $addFields: {
651
+ session_count: { $ifNull: [{ $arrayElemAt: ['$_sessions.count', 0] }, 0] },
652
+ elevenlabs_cost: { $ifNull: [{ $arrayElemAt: ['$_sessions.total_cost', 0] }, 0] },
653
+ },
654
+ },
655
+ {
656
+ $project: {
657
+ contact_name: 1, contact_phone: 1,
658
+ ai_message_count: 1, period_message_count: 1,
659
+ session_count: 1, elevenlabs_cost: 1,
660
+ last_message_at: 1, createdAt: 1,
661
+ },
662
+ },
663
+ { $sort: { last_message_at: -1 } },
664
+ {
665
+ $facet: {
666
+ data: [{ $skip: skipChats }, { $limit: limit }],
667
+ count: [{ $count: 'total' }],
668
+ summary: [{
669
+ $group: {
670
+ _id: null,
671
+ total_messages: { $sum: '$period_message_count' },
672
+ total_ai_messages: { $sum: '$ai_message_count' },
673
+ total_elevenlabs_cost: { $sum: '$elevenlabs_cost' },
674
+ total_sessions: { $sum: '$session_count' },
675
+ },
676
+ }],
677
+ },
678
+ },
679
+ ]),
680
+ ]);
681
+ const cs = callSummaryRows[0] || {
682
+ total: 0, total_duration: 0, total_cost: 0,
683
+ completed: 0, failed: 0, no_answer: 0, voicemail: 0, transferred: 0,
684
+ };
685
+ const chatData = sessionResult[0]?.data || [];
686
+ const totalBillableChats = sessionResult[0]?.count?.[0]?.total || 0;
687
+ const ss = sessionResult[0]?.summary?.[0] || {
688
+ total_messages: 0, total_ai_messages: 0, total_elevenlabs_cost: 0, total_sessions: 0,
689
+ };
690
+ return {
691
+ summary: {
692
+ total_calls: cs.total,
693
+ total_call_duration_seconds: cs.total_duration,
694
+ total_elevenlabs_cost: cs.total_cost,
695
+ calls_by_status: {
696
+ completed: cs.completed,
697
+ failed: cs.failed,
698
+ no_answer: cs.no_answer,
699
+ voicemail: cs.voicemail,
700
+ transferred: cs.transferred,
701
+ },
702
+ total_billable_chats: totalBillableChats,
703
+ total_chat_sessions: ss.total_sessions,
704
+ total_chat_messages: ss.total_messages,
705
+ total_chat_ai_messages: ss.total_ai_messages,
706
+ total_chat_elevenlabs_cost: ss.total_elevenlabs_cost,
707
+ date_range: { from: from.toISOString(), to: to.toISOString() },
708
+ },
709
+ calls: {
710
+ items: callItems,
711
+ total: cs.total,
712
+ page: pageCalls,
713
+ totalPages: Math.ceil(cs.total / limit),
714
+ },
715
+ whatsapp_chats: {
716
+ items: chatData,
717
+ total: totalBillableChats,
718
+ page: pageChats,
719
+ totalPages: Math.ceil(totalBillableChats / limit),
720
+ },
721
+ };
722
+ });
723
+ // POST /admin/tenants/:id/sync-whatsapp-costs — fetch ElevenLabs credits for sessions missing cost
724
+ fastify.post('/tenants/:id/sync-whatsapp-costs', async (request, reply) => {
725
+ const { id } = request.params;
726
+ const tenant = await tenant_model_1.default.findById(id).lean();
727
+ if (!tenant) {
728
+ return reply.status(404).send({ error: 'Tenant not found' });
729
+ }
730
+ const sessions = await whatsapp_session_model_1.default.find({
731
+ tenant_id: new mongoose_1.default.Types.ObjectId(id),
732
+ elevenlabs_conversation_id: { $ne: null },
733
+ elevenlabs_cost: null,
734
+ }).lean();
735
+ let updated = 0;
736
+ let failed = 0;
737
+ for (const session of sessions) {
738
+ try {
739
+ const conv = await elevenlabs_service_1.default.getConversation(session.elevenlabs_conversation_id);
740
+ const cost = conv.metadata?.cost;
741
+ if (cost != null) {
742
+ await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_cost: cost });
743
+ updated++;
744
+ }
745
+ }
746
+ catch (err) {
747
+ const status = err.statusCode
748
+ || err.status;
749
+ if (status === 404) {
750
+ await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_cost: 0 });
751
+ updated++;
752
+ }
753
+ else {
754
+ failed++;
755
+ }
756
+ }
757
+ }
758
+ return { total: sessions.length, updated, failed };
759
+ });
760
+ // POST /admin/payments — record a new payment
761
+ fastify.post('/payments', async (request, reply) => {
762
+ const { tenant_id, type, plan_slug, amount, extra_minutes, period_start, period_end, payment_method, notes } = request.body;
763
+ if (!tenant_id || !plan_slug || amount == null || !period_start || !period_end) {
764
+ return reply.status(400).send({ error: 'tenant_id, plan_slug, amount, period_start, period_end are required' });
765
+ }
766
+ const paymentType = type === 'top_up' ? 'top_up' : 'subscription';
767
+ if (paymentType === 'top_up' && (!extra_minutes || extra_minutes <= 0)) {
768
+ return reply.status(400).send({ error: 'Ek dakika (extra_minutes) top-up icin zorunludur' });
769
+ }
770
+ const tenant = await tenant_model_1.default.findById(tenant_id);
771
+ if (!tenant) {
772
+ return reply.status(404).send({ error: 'Tenant not found' });
773
+ }
774
+ // Lookup plan name from slug
775
+ const plan = await plan_model_1.default.findOne({ slug: plan_slug }).lean();
776
+ const plan_name = paymentType === 'top_up'
777
+ ? `Ek Dakika (${extra_minutes} dk)`
778
+ : (plan?.name || plan_slug);
779
+ const user = request.user;
780
+ const payment = await payment_model_1.default.create({
781
+ tenant_id,
782
+ type: paymentType,
783
+ plan_slug,
784
+ plan_name,
785
+ amount,
786
+ extra_minutes: paymentType === 'top_up' ? (extra_minutes || 0) : 0,
787
+ period_start: new Date(period_start),
788
+ period_end: new Date(period_end),
789
+ payment_method: payment_method || 'cash',
790
+ notes: notes || '',
791
+ recorded_by: user.id
792
+ });
793
+ // Snapshot previous billing period (subscription only, if it's expired)
794
+ if (paymentType === 'subscription') {
795
+ const now = new Date();
796
+ const prevPayment = await payment_model_1.default.findOne({
797
+ tenant_id,
798
+ type: 'subscription',
799
+ _id: { $ne: payment._id },
800
+ period_end: { $lt: now },
801
+ }).sort({ period_end: -1 }).lean();
802
+ if (prevPayment) {
803
+ (0, billing_service_1.snapshotPeriod)(tenant_id, prevPayment.period_start, prevPayment.period_end, prevPayment._id.toString())
804
+ .catch(err => logger_1.default.error('Failed to snapshot billing period', { error: err.message }));
805
+ }
806
+ }
807
+ return reply.status(201).send({ payment });
808
+ });
809
+ // ─── ElevenLabs API (read-only proxies) ───────────────────
810
+ // GET /admin/elevenlabs/agents — list ElevenLabs agents
811
+ fastify.get('/elevenlabs/agents', async (request) => {
812
+ const { page_size, search, cursor } = request.query;
813
+ return elevenlabs_service_1.default.listAgents({
814
+ pageSize: page_size ? parseInt(page_size, 10) : undefined,
815
+ search: search || undefined,
816
+ cursor: cursor || undefined,
817
+ });
818
+ });
819
+ // GET /admin/elevenlabs/agents/:agentId — get single ElevenLabs agent
820
+ fastify.get('/elevenlabs/agents/:agentId', async (request) => {
821
+ const { agentId } = request.params;
822
+ return elevenlabs_service_1.default.getAgent(agentId);
823
+ });
824
+ // GET /admin/elevenlabs/conversations — list conversations
825
+ fastify.get('/elevenlabs/conversations', async (request) => {
826
+ const { agent_id, page_size, cursor, call_successful, search } = request.query;
827
+ return elevenlabs_service_1.default.listConversations({
828
+ agentId: agent_id || undefined,
829
+ pageSize: page_size ? parseInt(page_size, 10) : undefined,
830
+ cursor: cursor || undefined,
831
+ callSuccessful: call_successful,
832
+ search: search || undefined,
833
+ });
834
+ });
835
+ // GET /admin/elevenlabs/conversations/:conversationId — get conversation detail
836
+ fastify.get('/elevenlabs/conversations/:conversationId', async (request) => {
837
+ const { conversationId } = request.params;
838
+ return elevenlabs_service_1.default.getConversation(conversationId);
839
+ });
840
+ // GET /admin/elevenlabs/conversations/review — enriched list with evaluation status
841
+ fastify.get('/elevenlabs/conversations/review', async (request) => {
842
+ const { agent_id, page_size, cursor, call_successful } = request.query;
843
+ const currentUser = request.user;
844
+ // Get all active claims by OTHER users (exclude our own claims)
845
+ const otherClaims = await ConversationClaim_1.default.find({
846
+ user_id: { $ne: currentUser.id },
847
+ expires_at: { $gt: new Date() },
848
+ }).lean();
849
+ const claimedByOthers = new Set(otherClaims.map((c) => c.conversation_id));
850
+ const targetSize = page_size ? parseInt(page_size, 10) : 10;
851
+ const results = [];
852
+ let currentCursor = cursor || undefined;
853
+ let hasMore = true;
854
+ let nextCursor;
855
+ // Keep fetching pages until we have enough non-text conversations or run out
856
+ while (results.length < targetSize && hasMore) {
857
+ const listResult = await elevenlabs_service_1.default.listConversations({
858
+ agentId: agent_id || undefined,
859
+ pageSize: 25, // fetch in chunks of 25
860
+ cursor: currentCursor,
861
+ callSuccessful: call_successful,
862
+ });
863
+ hasMore = listResult.hasMore || false;
864
+ nextCursor = listResult.nextCursor;
865
+ currentCursor = listResult.nextCursor;
866
+ if (listResult.conversations.length === 0)
867
+ break;
868
+ // Quick filter: skip text agents before expensive detail fetch
869
+ const nonText = listResult.conversations.filter((c) => !(c.agentName || '').toLowerCase().includes('text'));
870
+ if (nonText.length === 0)
871
+ continue;
872
+ const conversationIds = nonText.map((c) => c.conversationId);
873
+ // Skip already-evaluated and claimed-by-others conversations
874
+ const evaluations = await ConversationEvaluation_1.default.find({
875
+ conversation_id: { $in: conversationIds },
876
+ }).lean();
877
+ const evaluatedIds = new Set(evaluations.map((e) => e.conversation_id));
878
+ const unevaluated = nonText.filter((c) => !evaluatedIds.has(c.conversationId) &&
879
+ !claimedByOthers.has(c.conversationId));
880
+ if (unevaluated.length === 0)
881
+ continue;
882
+ const enriched = await Promise.all(unevaluated.map(async (c) => {
883
+ try {
884
+ const detail = await elevenlabs_service_1.default.getConversation(c.conversationId);
885
+ const phoneCall = detail.metadata?.phoneCall;
886
+ return {
887
+ conversationId: c.conversationId,
888
+ agentId: c.agentId,
889
+ agentName: c.agentName,
890
+ status: c.status,
891
+ callSuccessful: c.callSuccessful,
892
+ callDurationSecs: c.callDurationSecs,
893
+ messageCount: c.messageCount,
894
+ direction: c.direction,
895
+ startTimeUnixSecs: c.startTimeUnixSecs,
896
+ transcriptSummary: c.transcriptSummary,
897
+ externalNumber: phoneCall ? phoneCall.externalNumber : null,
898
+ agentNumber: phoneCall ? phoneCall.agentNumber : null,
899
+ terminationReason: detail.metadata?.terminationReason || null,
900
+ cost: detail.metadata?.cost ?? null,
901
+ hasAudio: detail.hasAudio ?? false,
902
+ hasEvaluation: false,
903
+ evaluation: null,
904
+ };
905
+ }
906
+ catch {
907
+ return {
908
+ conversationId: c.conversationId,
909
+ agentId: c.agentId,
910
+ agentName: c.agentName,
911
+ status: c.status,
912
+ callSuccessful: c.callSuccessful,
913
+ callDurationSecs: c.callDurationSecs,
914
+ messageCount: c.messageCount,
915
+ direction: c.direction,
916
+ startTimeUnixSecs: c.startTimeUnixSecs,
917
+ transcriptSummary: c.transcriptSummary,
918
+ externalNumber: null,
919
+ agentNumber: null,
920
+ terminationReason: null,
921
+ cost: null,
922
+ hasAudio: false,
923
+ hasEvaluation: false,
924
+ evaluation: null,
925
+ };
926
+ }
927
+ }));
928
+ results.push(...enriched);
929
+ }
930
+ return {
931
+ conversations: results.slice(0, targetSize),
932
+ hasMore: hasMore || results.length > targetSize,
933
+ nextCursor,
934
+ };
935
+ });
936
+ // GET /admin/elevenlabs/conversations/:conversationId/audio — stream audio
937
+ fastify.get('/elevenlabs/conversations/:conversationId/audio', async (request, reply) => {
938
+ const { conversationId } = request.params;
939
+ try {
940
+ const audioResult = await elevenlabs_service_1.default.getConversationAudio(conversationId);
941
+ reply
942
+ .type('audio/mpeg')
943
+ .header('Cache-Control', 'public, max-age=86400')
944
+ .header('Accept-Ranges', 'bytes');
945
+ // Handle different return types from the SDK
946
+ if (audioResult instanceof ReadableStream) {
947
+ // Web ReadableStream → Node Readable
948
+ const nodeStream = stream_1.Readable.fromWeb(audioResult);
949
+ return reply.send(nodeStream);
950
+ }
951
+ else if (Buffer.isBuffer(audioResult)) {
952
+ return reply.send(audioResult);
953
+ }
954
+ else if (audioResult && typeof audioResult.pipe === 'function') {
955
+ // Already a Node.js Readable stream
956
+ return reply.send(audioResult);
957
+ }
958
+ else if (audioResult && typeof audioResult[Symbol.asyncIterator] === 'function') {
959
+ // Async iterable
960
+ const chunks = [];
961
+ for await (const chunk of audioResult) {
962
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
963
+ }
964
+ return reply.send(Buffer.concat(chunks));
965
+ }
966
+ else {
967
+ // Fallback: try to send as-is
968
+ return reply.send(audioResult);
969
+ }
970
+ }
971
+ catch (error) {
972
+ logger_1.default.error('Failed to stream conversation audio', { conversationId, error: error.message });
973
+ return reply.status(500).send({ error: 'Audio yuklenemedi' });
974
+ }
975
+ });
976
+ // GET /admin/elevenlabs/evaluations — list all evaluations with pagination
977
+ fastify.get('/elevenlabs/evaluations', async (request) => {
978
+ const { page = '1', limit = '20', evaluator_id, skipped, min_score, max_score } = request.query;
979
+ const pageNum = Math.max(1, parseInt(page, 10));
980
+ const limitNum = Math.min(100, Math.max(1, parseInt(limit, 10)));
981
+ const skip = (pageNum - 1) * limitNum;
982
+ const filter = {};
983
+ if (evaluator_id)
984
+ filter.evaluator_id = evaluator_id;
985
+ if (skipped === 'true')
986
+ filter.skipped = true;
987
+ if (skipped === 'false')
988
+ filter.skipped = { $ne: true };
989
+ if (min_score)
990
+ filter.overall_score = { ...(filter.overall_score || {}), $gte: parseFloat(min_score) };
991
+ if (max_score)
992
+ filter.overall_score = { ...(filter.overall_score || {}), $lte: parseFloat(max_score) };
993
+ const [rawEvaluations, total] = await Promise.all([
994
+ ConversationEvaluation_1.default.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limitNum).lean(),
995
+ ConversationEvaluation_1.default.countDocuments(filter),
996
+ ]);
997
+ // Enrich evaluations with conversation details from ElevenLabs
998
+ const evaluations = await Promise.all(rawEvaluations.map(async (ev) => {
999
+ try {
1000
+ const detail = await elevenlabs_service_1.default.getConversation(ev.conversation_id);
1001
+ const phoneCall = detail.metadata?.phoneCall;
1002
+ return {
1003
+ ...ev,
1004
+ agentName: detail.agentName || null,
1005
+ agentId: detail.agentId || null,
1006
+ callDurationSecs: detail.metadata?.callDurationSecs || 0,
1007
+ startTimeUnixSecs: detail.metadata?.startTimeUnixSecs || 0,
1008
+ externalNumber: phoneCall ? phoneCall.externalNumber : null,
1009
+ cost: detail.metadata?.cost ?? null,
1010
+ callSuccessful: detail.analysis?.callSuccessful || null,
1011
+ transcriptSummary: detail.analysis?.transcriptSummary || null,
1012
+ };
1013
+ }
1014
+ catch {
1015
+ return { ...ev, agentName: null, agentId: null, callDurationSecs: 0, startTimeUnixSecs: 0, externalNumber: null, cost: null, callSuccessful: null, transcriptSummary: null };
1016
+ }
1017
+ }));
1018
+ // Aggregate stats
1019
+ const stats = await ConversationEvaluation_1.default.aggregate([
1020
+ { $match: { skipped: { $ne: true } } },
1021
+ {
1022
+ $group: {
1023
+ _id: null,
1024
+ total: { $sum: 1 },
1025
+ avg_score: { $avg: '$overall_score' },
1026
+ avg_patient_audio: { $avg: '$patient_audio_quality' },
1027
+ avg_ai_comprehension: { $avg: '$ai_comprehension' },
1028
+ avg_ai_speech: { $avg: '$ai_speech_quality' },
1029
+ avg_ai_content: { $avg: '$ai_content_quality' },
1030
+ avg_no_unnecessary: { $avg: '$no_unnecessary_content' },
1031
+ avg_patient_goal: { $avg: '$patient_reached_goal' },
1032
+ },
1033
+ },
1034
+ ]);
1035
+ const skippedCount = await ConversationEvaluation_1.default.countDocuments({ skipped: true });
1036
+ // Per-evaluator stats
1037
+ const evaluatorStats = await ConversationEvaluation_1.default.aggregate([
1038
+ { $match: { skipped: { $ne: true } } },
1039
+ {
1040
+ $group: {
1041
+ _id: '$evaluator_id',
1042
+ name: { $first: '$evaluator_name' },
1043
+ count: { $sum: 1 },
1044
+ avg_score: { $avg: '$overall_score' },
1045
+ },
1046
+ },
1047
+ { $sort: { count: -1 } },
1048
+ ]);
1049
+ return {
1050
+ evaluations,
1051
+ total,
1052
+ page: pageNum,
1053
+ totalPages: Math.ceil(total / limitNum),
1054
+ stats: stats[0] || null,
1055
+ skippedCount,
1056
+ evaluatorStats,
1057
+ };
1058
+ });
1059
+ // GET /admin/elevenlabs/conversations/:conversationId/evaluation
1060
+ fastify.get('/elevenlabs/conversations/:conversationId/evaluation', async (request, reply) => {
1061
+ const { conversationId } = request.params;
1062
+ const evaluation = await ConversationEvaluation_1.default.findOne({ conversation_id: conversationId }).lean();
1063
+ if (!evaluation) {
1064
+ return reply.status(404).send({ error: 'Evaluation not found' });
1065
+ }
1066
+ return evaluation;
1067
+ });
1068
+ // PUT /admin/elevenlabs/conversations/:conversationId/evaluation — upsert
1069
+ fastify.put('/elevenlabs/conversations/:conversationId/evaluation', async (request) => {
1070
+ const { conversationId } = request.params;
1071
+ const { patient_audio_quality, ai_comprehension, ai_speech_quality, ai_content_quality, no_unnecessary_content, patient_reached_goal, notes } = request.body;
1072
+ const jwtUser = request.user;
1073
+ const dbUser = await auth_model_1.default.findById(jwtUser.id).select('name').lean();
1074
+ const evaluatorName = dbUser?.name || 'Superadmin';
1075
+ const criteria = [patient_audio_quality, ai_comprehension, ai_speech_quality, ai_content_quality, no_unnecessary_content, patient_reached_goal].filter((v) => v > 0);
1076
+ const overall_score = criteria.length > 0 ? Math.round((criteria.reduce((a, b) => a + b, 0) / criteria.length) * 10) / 10 : 0;
1077
+ const evaluation = await ConversationEvaluation_1.default.findOneAndUpdate({ conversation_id: conversationId }, {
1078
+ $set: {
1079
+ evaluator_id: jwtUser.id,
1080
+ evaluator_name: evaluatorName,
1081
+ skipped: false,
1082
+ patient_audio_quality: patient_audio_quality || null,
1083
+ ai_comprehension: ai_comprehension || null,
1084
+ ai_speech_quality: ai_speech_quality || null,
1085
+ ai_content_quality: ai_content_quality || null,
1086
+ no_unnecessary_content: no_unnecessary_content || null,
1087
+ patient_reached_goal: patient_reached_goal || null,
1088
+ overall_score,
1089
+ notes: notes || '',
1090
+ },
1091
+ }, { upsert: true, new: true });
1092
+ return evaluation;
1093
+ });
1094
+ // DELETE /admin/elevenlabs/conversations/:conversationId/evaluation
1095
+ fastify.delete('/elevenlabs/conversations/:conversationId/evaluation', async (request, reply) => {
1096
+ const { conversationId } = request.params;
1097
+ const result = await ConversationEvaluation_1.default.deleteOne({ conversation_id: conversationId });
1098
+ if (result.deletedCount === 0) {
1099
+ return reply.status(404).send({ error: 'Evaluation not found' });
1100
+ }
1101
+ return { deleted: true };
1102
+ });
1103
+ // POST /admin/elevenlabs/conversations/:conversationId/claim — claim for review (5 min TTL)
1104
+ fastify.post('/elevenlabs/conversations/:conversationId/claim', async (request) => {
1105
+ const { conversationId } = request.params;
1106
+ const jwtUser = request.user;
1107
+ const dbUser = await auth_model_1.default.findById(jwtUser.id).select('name').lean();
1108
+ await ConversationClaim_1.default.findOneAndUpdate({ conversation_id: conversationId }, {
1109
+ $set: {
1110
+ user_id: jwtUser.id,
1111
+ user_name: dbUser?.name || 'Superadmin',
1112
+ expires_at: new Date(Date.now() + 5 * 60 * 1000), // 5 min TTL
1113
+ },
1114
+ }, { upsert: true });
1115
+ return { claimed: true };
1116
+ });
1117
+ // POST /admin/elevenlabs/conversations/:conversationId/skip — mark as not evaluable
1118
+ fastify.post('/elevenlabs/conversations/:conversationId/skip', async (request) => {
1119
+ const { conversationId } = request.params;
1120
+ const jwtUser = request.user;
1121
+ const dbUser = await auth_model_1.default.findById(jwtUser.id).select('name').lean();
1122
+ const evaluatorName = dbUser?.name || 'Superadmin';
1123
+ const evaluation = await ConversationEvaluation_1.default.findOneAndUpdate({ conversation_id: conversationId }, {
1124
+ $set: {
1125
+ evaluator_id: jwtUser.id,
1126
+ evaluator_name: evaluatorName,
1127
+ skipped: true,
1128
+ notes: request.body?.notes || '',
1129
+ overall_score: 0,
1130
+ },
1131
+ }, { upsert: true, new: true });
1132
+ return evaluation;
1133
+ });
1134
+ // GET /admin/elevenlabs/inbound-calls — list SIP trunk inbound calls with phone metadata
1135
+ fastify.get('/elevenlabs/inbound-calls', async (request) => {
1136
+ const { agent_id, page_size, cursor, call_successful, search } = request.query;
1137
+ const listResult = await elevenlabs_service_1.default.listConversations({
1138
+ agentId: agent_id || undefined,
1139
+ pageSize: page_size ? parseInt(page_size, 10) : undefined,
1140
+ cursor: cursor || undefined,
1141
+ callSuccessful: call_successful,
1142
+ search: search || undefined,
1143
+ conversationInitiationSource: 'sip_trunk',
1144
+ });
1145
+ // Enrich each conversation with phone call metadata from detail endpoint
1146
+ const enriched = await Promise.all(listResult.conversations.map(async (c) => {
1147
+ try {
1148
+ const detail = await elevenlabs_service_1.default.getConversation(c.conversationId);
1149
+ const phoneCall = detail.metadata?.phoneCall;
1150
+ return {
1151
+ conversationId: c.conversationId,
1152
+ agentId: c.agentId,
1153
+ agentName: c.agentName,
1154
+ status: c.status,
1155
+ callSuccessful: c.callSuccessful,
1156
+ callDurationSecs: c.callDurationSecs,
1157
+ messageCount: c.messageCount,
1158
+ direction: c.direction,
1159
+ startTimeUnixSecs: c.startTimeUnixSecs,
1160
+ rating: c.rating,
1161
+ transcriptSummary: c.transcriptSummary,
1162
+ phoneCallType: phoneCall ? phoneCall.type : null,
1163
+ callSid: phoneCall ? phoneCall.callSid : null,
1164
+ agentNumber: phoneCall ? phoneCall.agentNumber : null,
1165
+ externalNumber: phoneCall ? phoneCall.externalNumber : null,
1166
+ phoneNumberId: phoneCall ? phoneCall.phoneNumberId : null,
1167
+ terminationReason: detail.metadata?.terminationReason || null,
1168
+ cost: detail.metadata?.cost ?? null,
1169
+ };
1170
+ }
1171
+ catch {
1172
+ return {
1173
+ conversationId: c.conversationId,
1174
+ agentId: c.agentId,
1175
+ agentName: c.agentName,
1176
+ status: c.status,
1177
+ callSuccessful: c.callSuccessful,
1178
+ callDurationSecs: c.callDurationSecs,
1179
+ messageCount: c.messageCount,
1180
+ direction: c.direction,
1181
+ startTimeUnixSecs: c.startTimeUnixSecs,
1182
+ rating: c.rating,
1183
+ transcriptSummary: c.transcriptSummary,
1184
+ phoneCallType: null,
1185
+ callSid: null,
1186
+ agentNumber: null,
1187
+ externalNumber: null,
1188
+ phoneNumberId: null,
1189
+ terminationReason: null,
1190
+ cost: null,
1191
+ };
1192
+ }
1193
+ }));
1194
+ return {
1195
+ conversations: enriched,
1196
+ hasMore: listResult.hasMore,
1197
+ nextCursor: listResult.nextCursor,
1198
+ };
1199
+ });
1200
+ // GET /admin/elevenlabs/batch-calls — list batch calls
1201
+ fastify.get('/elevenlabs/batch-calls', async (request) => {
1202
+ const { limit, last_doc } = request.query;
1203
+ return elevenlabs_service_1.default.listBatchCalls({
1204
+ limit: limit ? parseInt(limit, 10) : undefined,
1205
+ lastDoc: last_doc || undefined,
1206
+ });
1207
+ });
1208
+ // GET /admin/elevenlabs/batch-calls/:batchId — get batch call detail
1209
+ fastify.get('/elevenlabs/batch-calls/:batchId', async (request) => {
1210
+ const { batchId } = request.params;
1211
+ return elevenlabs_service_1.default.getBatchCall(batchId);
1212
+ });
1213
+ // POST /admin/elevenlabs/batch-calls — trigger a new batch call
1214
+ fastify.post('/elevenlabs/batch-calls', async (request, reply) => {
1215
+ const { name, agent_id, phone_numbers } = request.body;
1216
+ if (!name || !agent_id || !phone_numbers?.length) {
1217
+ return reply.status(400).send({ error: 'name, agent_id, and phone_numbers are required' });
1218
+ }
1219
+ const recipients = phone_numbers
1220
+ .map((n) => n.trim())
1221
+ .filter(Boolean)
1222
+ .map((n) => ({ phoneNumber: n.startsWith('+') ? n : `+${n}` }));
1223
+ if (recipients.length === 0) {
1224
+ return reply.status(400).send({ error: 'At least one valid phone number is required' });
1225
+ }
1226
+ const result = await elevenlabs_service_1.default.submitBatchCall({
1227
+ callName: name,
1228
+ agentId: agent_id,
1229
+ recipients,
1230
+ });
1231
+ return reply.status(201).send(result);
1232
+ });
1233
+ // ─── ElevenLabs Webhook Management ──────────────────────────
1234
+ // GET /admin/elevenlabs/webhooks — list workspace webhooks
1235
+ fastify.get('/elevenlabs/webhooks', async () => {
1236
+ return elevenlabs_service_1.default.listWebhooks({ includeUsages: true });
1237
+ });
1238
+ // POST /admin/elevenlabs/webhooks — create workspace webhook
1239
+ fastify.post('/elevenlabs/webhooks', async (request, reply) => {
1240
+ const { name, webhookUrl } = request.body;
1241
+ if (!name || !webhookUrl) {
1242
+ return reply.status(400).send({ error: 'name and webhookUrl are required' });
1243
+ }
1244
+ return elevenlabs_service_1.default.createWebhook({
1245
+ settings: { authType: 'hmac', name, webhookUrl },
1246
+ });
1247
+ });
1248
+ // PATCH /admin/elevenlabs/webhooks/:webhookId — update webhook
1249
+ fastify.patch('/elevenlabs/webhooks/:webhookId', async (request) => {
1250
+ const { webhookId } = request.params;
1251
+ const { isDisabled, name } = request.body;
1252
+ return elevenlabs_service_1.default.updateWebhook(webhookId, {
1253
+ isDisabled: isDisabled ?? false,
1254
+ name: name ?? '',
1255
+ });
1256
+ });
1257
+ // DELETE /admin/elevenlabs/webhooks/:webhookId — delete webhook
1258
+ fastify.delete('/elevenlabs/webhooks/:webhookId', async (request) => {
1259
+ const { webhookId } = request.params;
1260
+ return elevenlabs_service_1.default.deleteWebhook(webhookId);
1261
+ });
1262
+ // PATCH /admin/elevenlabs/agents/:agentId/webhook — assign post-call webhook to agent
1263
+ fastify.patch('/elevenlabs/agents/:agentId/webhook', async (request, reply) => {
1264
+ const { agentId } = request.params;
1265
+ const { postCallWebhookId, events } = request.body;
1266
+ if (!postCallWebhookId) {
1267
+ return reply.status(400).send({ error: 'postCallWebhookId is required' });
1268
+ }
1269
+ return elevenlabs_service_1.default.updateAgent(agentId, {
1270
+ platformSettings: {
1271
+ workspaceOverrides: {
1272
+ webhooks: {
1273
+ postCallWebhookId,
1274
+ events: events || ['transcript'],
1275
+ },
1276
+ },
1277
+ },
1278
+ });
1279
+ });
1280
+ // DELETE /admin/elevenlabs/agents/:agentId/webhook — remove post-call webhook from agent
1281
+ fastify.delete('/elevenlabs/agents/:agentId/webhook', async (request) => {
1282
+ const { agentId } = request.params;
1283
+ return elevenlabs_service_1.default.updateAgent(agentId, {
1284
+ platformSettings: {
1285
+ workspaceOverrides: {
1286
+ webhooks: {
1287
+ postCallWebhookId: '',
1288
+ events: [],
1289
+ },
1290
+ },
1291
+ },
1292
+ });
1293
+ });
1294
+ // ─── WhatsApp Bridge Provider ────────────────────────────────
1295
+ // GET /admin/whatsapp-providers — List available bridge providers
1296
+ fastify.get('/whatsapp-providers', async () => {
1297
+ const unipileService = (await Promise.resolve().then(() => __importStar(require('../../services/unipile.service')))).default;
1298
+ return { providers: unipileService.getAvailableProviders() };
1299
+ });
1300
+ // PUT /admin/tenants/:id/whatsapp-bridge — Set tenant's bridge provider
1301
+ fastify.put('/tenants/:id/whatsapp-bridge', async (request, reply) => {
1302
+ const { id } = request.params;
1303
+ const { provider } = request.body;
1304
+ if (!['unipile', 'baileys'].includes(provider)) {
1305
+ return reply.status(400).send({ error: 'Invalid provider. Must be "unipile" or "baileys"' });
1306
+ }
1307
+ const unipileService = (await Promise.resolve().then(() => __importStar(require('../../services/unipile.service')))).default;
1308
+ if (!unipileService.isProviderReady(provider)) {
1309
+ return reply.status(400).send({ error: `Provider "${provider}" is not configured on this server` });
1310
+ }
1311
+ const tenant = await tenant_model_1.default.findById(id);
1312
+ if (!tenant) {
1313
+ return reply.status(404).send({ error: 'Tenant not found' });
1314
+ }
1315
+ // Refuse if tenant has active connection — must disconnect first
1316
+ if (tenant.settings?.whatsapp_agent?.unipile_account_id) {
1317
+ return reply.status(400).send({
1318
+ error: 'Tenant has an active WhatsApp connection. Disconnect first before switching providers.',
1319
+ });
1320
+ }
1321
+ await tenant_model_1.default.findByIdAndUpdate(id, {
1322
+ $set: { 'settings.whatsapp_agent.whatsapp_bridge_provider': provider }
1323
+ });
1324
+ return { success: true, provider };
1325
+ });
1326
+ // ─── Leads (cross-tenant) ──────────────────────────────────
1327
+ // GET /admin/leads — all leads across tenants
1328
+ fastify.get('/leads', async (request) => {
1329
+ const page = parseInt(request.query.page || '1', 10);
1330
+ const limit = Math.min(parseInt(request.query.limit || '30', 10), 100);
1331
+ const skip = (page - 1) * limit;
1332
+ const filter = {};
1333
+ if (request.query.tenant_id)
1334
+ filter.tenant_id = request.query.tenant_id;
1335
+ if (request.query.lead_status) {
1336
+ filter.lead_status = request.query.lead_status;
1337
+ }
1338
+ else {
1339
+ filter.lead_status = { $ne: 'expired' };
1340
+ }
1341
+ if (request.query.channel)
1342
+ filter.channel = request.query.channel;
1343
+ if (request.query.category)
1344
+ filter.categories = request.query.category;
1345
+ if (request.query.date_from || request.query.date_to) {
1346
+ const dateFilter = {};
1347
+ if (request.query.date_from)
1348
+ dateFilter.$gte = new Date(request.query.date_from);
1349
+ if (request.query.date_to) {
1350
+ const to = new Date(request.query.date_to);
1351
+ to.setHours(23, 59, 59, 999);
1352
+ dateFilter.$lte = to;
1353
+ }
1354
+ filter.createdAt = dateFilter;
1355
+ }
1356
+ const [leads, total] = await Promise.all([
1357
+ lead_model_1.default.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
1358
+ lead_model_1.default.countDocuments(filter)
1359
+ ]);
1360
+ // Enrich with tenant names
1361
+ const tenantIds = [...new Set(leads.map(l => String(l.tenant_id)))];
1362
+ const tenants = await tenant_model_1.default.find({ _id: { $in: tenantIds } }).select('name').lean();
1363
+ const tenantMap = new Map(tenants.map(t => [String(t._id), t.name]));
1364
+ const enriched = leads.map(l => ({
1365
+ ...l,
1366
+ tenant_name: tenantMap.get(String(l.tenant_id)) || '-',
1367
+ }));
1368
+ return {
1369
+ leads: enriched,
1370
+ total,
1371
+ page,
1372
+ totalPages: Math.ceil(total / limit)
1373
+ };
1374
+ });
1375
+ // GET /admin/leads/stats — cross-tenant lead stats
1376
+ fastify.get('/leads/stats', async () => {
1377
+ const [total, byStatus, byChannel] = await Promise.all([
1378
+ lead_model_1.default.countDocuments({ lead_status: { $ne: 'expired' } }),
1379
+ lead_model_1.default.aggregate([
1380
+ { $match: { lead_status: { $ne: 'expired' } } },
1381
+ { $group: { _id: '$lead_status', count: { $sum: 1 } } }
1382
+ ]),
1383
+ lead_model_1.default.aggregate([
1384
+ { $match: { lead_status: { $ne: 'expired' } } },
1385
+ { $group: { _id: '$channel', count: { $sum: 1 } } }
1386
+ ])
1387
+ ]);
1388
+ const statusMap = {};
1389
+ for (const s of byStatus)
1390
+ statusMap[s._id] = s.count;
1391
+ const channelMap = {};
1392
+ for (const c of byChannel)
1393
+ channelMap[c._id] = c.count;
1394
+ return {
1395
+ stats: {
1396
+ total,
1397
+ new: statusMap['new'] || 0,
1398
+ contacted: statusMap['contacted'] || 0,
1399
+ appointment_scheduled: statusMap['appointment_scheduled'] || 0,
1400
+ completed: statusMap['completed'] || 0,
1401
+ lost: statusMap['lost'] || 0,
1402
+ phone: channelMap['phone'] || 0,
1403
+ whatsapp: channelMap['whatsapp'] || 0,
1404
+ }
1405
+ };
1406
+ });
1407
+ // ─── WhatsApp (cross-tenant, read-only) ────────────────────
1408
+ // GET /admin/whatsapp/stats — cross-tenant WhatsApp stats
1409
+ fastify.get('/whatsapp/stats', async () => {
1410
+ const [totalChats, closedChats, waitingSessions, activeSessions, totalMessages] = await Promise.all([
1411
+ whatsapp_chat_model_1.default.countDocuments({ deleted_at: null }),
1412
+ whatsapp_chat_model_1.default.countDocuments({ is_closed: true, deleted_at: null }),
1413
+ whatsapp_session_model_1.default.countDocuments({ status: 'waiting' }),
1414
+ whatsapp_session_model_1.default.countDocuments({ status: 'active' }),
1415
+ whatsapp_message_model_1.default.countDocuments(),
1416
+ ]);
1417
+ const humanTakeoverCount = await whatsapp_session_model_1.default.countDocuments({
1418
+ status: 'active',
1419
+ $or: [{ taken_over_by: { $ne: null } }, { taken_over_at: { $ne: null } }],
1420
+ });
1421
+ return {
1422
+ stats: {
1423
+ totalChats,
1424
+ aiChats: activeSessions - humanTakeoverCount,
1425
+ humanChats: humanTakeoverCount + waitingSessions,
1426
+ closedChats,
1427
+ waitingChats: waitingSessions,
1428
+ totalMessages,
1429
+ },
1430
+ };
1431
+ });
1432
+ // GET /admin/whatsapp/agents — list WhatsApp agents assigned to tenants (for test chat)
1433
+ fastify.get('/whatsapp/agents', async () => {
1434
+ const tenants = await tenant_model_1.default.find({
1435
+ 'settings.whatsapp_agent.agent_id': { $ne: null },
1436
+ })
1437
+ .select('name settings.whatsapp_agent.agent_id settings.whatsapp_agent.connected_phone')
1438
+ .lean();
1439
+ const agentIds = tenants
1440
+ .map(t => t.settings?.whatsapp_agent?.agent_id)
1441
+ .filter(Boolean);
1442
+ const agents = await agent_model_1.default.find({ _id: { $in: agentIds } })
1443
+ .select('elevenlabs_agent_id is_active')
1444
+ .lean();
1445
+ const agentMap = new Map(agents.map(a => [String(a._id), a]));
1446
+ const result = tenants
1447
+ .map(t => {
1448
+ const agentObjId = t.settings?.whatsapp_agent?.agent_id;
1449
+ const agent = agentObjId ? agentMap.get(String(agentObjId)) : null;
1450
+ if (!agent || !agent.elevenlabs_agent_id)
1451
+ return null;
1452
+ return {
1453
+ tenant_id: String(t._id),
1454
+ tenant_name: t.name,
1455
+ agent_id: String(agent._id),
1456
+ elevenlabs_agent_id: agent.elevenlabs_agent_id,
1457
+ is_active: agent.is_active,
1458
+ connected_phone: t.settings?.whatsapp_agent?.connected_phone || null,
1459
+ };
1460
+ })
1461
+ .filter(Boolean);
1462
+ return { agents: result };
1463
+ });
1464
+ // GET /admin/whatsapp/contact-profiles — cross-tenant contact profiles (for test chat)
1465
+ fastify.get('/whatsapp/contact-profiles', async (request) => {
1466
+ const limit = Math.min(parseInt(request.query.limit || '50', 10), 100);
1467
+ const search = request.query.search?.trim();
1468
+ const filter = {};
1469
+ if (search) {
1470
+ filter.$or = [
1471
+ { display_name: { $regex: search, $options: 'i' } },
1472
+ { phone: { $regex: search, $options: 'i' } },
1473
+ ];
1474
+ }
1475
+ const profiles = await whatsapp_contact_profile_model_1.default.find(filter)
1476
+ .select('display_name phone ai_summary preferred_language tenant_id tags total_sessions')
1477
+ .sort({ last_seen_at: -1 })
1478
+ .limit(limit)
1479
+ .lean();
1480
+ // Truncate ai_summary to 100 chars for listing
1481
+ const result = profiles.map(p => ({
1482
+ ...p,
1483
+ ai_summary: p.ai_summary ? p.ai_summary.substring(0, 100) + (p.ai_summary.length > 100 ? '...' : '') : '',
1484
+ }));
1485
+ return { profiles: result };
1486
+ });
1487
+ // GET /admin/whatsapp/chats — list all chats across tenants
1488
+ fastify.get('/whatsapp/chats', async (request) => {
1489
+ const page = parseInt(request.query.page || '1', 10);
1490
+ const limit = Math.min(parseInt(request.query.limit || '20', 10), 100);
1491
+ const skip = (page - 1) * limit;
1492
+ const filter = { deleted_at: null };
1493
+ if (request.query.tenant_id) {
1494
+ filter.tenant_id = new mongoose_1.default.Types.ObjectId(request.query.tenant_id);
1495
+ }
1496
+ const pipeline = [
1497
+ { $match: filter },
1498
+ { $sort: { last_message_at: -1 } },
1499
+ {
1500
+ $lookup: {
1501
+ from: 'tenants',
1502
+ localField: 'tenant_id',
1503
+ foreignField: '_id',
1504
+ as: '_tenant',
1505
+ },
1506
+ },
1507
+ { $unwind: { path: '$_tenant', preserveNullAndEmptyArrays: true } },
1508
+ ];
1509
+ // Search by contact name/phone or tenant name
1510
+ if (request.query.search) {
1511
+ const regex = { $regex: request.query.search, $options: 'i' };
1512
+ pipeline.push({
1513
+ $match: {
1514
+ $or: [
1515
+ { contact_name: regex },
1516
+ { contact_phone: regex },
1517
+ { '_tenant.name': regex },
1518
+ ],
1519
+ },
1520
+ });
1521
+ }
1522
+ pipeline.push({
1523
+ $project: {
1524
+ tenant_id: 1,
1525
+ tenant_name: { $ifNull: ['$_tenant.name', 'Bilinmiyor'] },
1526
+ unipile_chat_id: 1,
1527
+ contact_name: 1,
1528
+ contact_phone: 1,
1529
+ is_closed: 1,
1530
+ active_session_id: 1,
1531
+ last_message_at: 1,
1532
+ last_message_preview: 1,
1533
+ message_count: 1,
1534
+ createdAt: 1,
1535
+ },
1536
+ }, {
1537
+ $facet: {
1538
+ data: [{ $skip: skip }, { $limit: limit }],
1539
+ count: [{ $count: 'total' }],
1540
+ },
1541
+ });
1542
+ const [result] = await whatsapp_chat_model_1.default.aggregate(pipeline);
1543
+ const chats = result?.data || [];
1544
+ const total = result?.count?.[0]?.total || 0;
1545
+ return { chats, total, page, totalPages: Math.ceil(total / limit) };
1546
+ });
1547
+ // GET /admin/whatsapp/chats/:chatId — single chat detail
1548
+ fastify.get('/whatsapp/chats/:chatId', async (request, reply) => {
1549
+ const { chatId } = request.params;
1550
+ const chat = await whatsapp_chat_model_1.default.findById(chatId).lean();
1551
+ if (!chat)
1552
+ return reply.status(404).send({ error: 'Chat not found' });
1553
+ const tenant = await tenant_model_1.default.findById(chat.tenant_id).select('name settings.whatsapp_agent.grace_period_seconds').lean();
1554
+ return {
1555
+ chat: {
1556
+ ...chat,
1557
+ tenant_name: tenant?.name || 'Bilinmiyor',
1558
+ grace_period_seconds: tenant?.settings?.whatsapp_agent?.grace_period_seconds ?? 180,
1559
+ },
1560
+ };
1561
+ });
1562
+ // GET /admin/whatsapp/chats/:chatId/messages — messages for a chat
1563
+ fastify.get('/whatsapp/chats/:chatId/messages', async (request, reply) => {
1564
+ const { chatId } = request.params;
1565
+ const page = parseInt(request.query.page || '1', 10);
1566
+ const limit = Math.min(parseInt(request.query.limit || '50', 10), 200);
1567
+ const skip = (page - 1) * limit;
1568
+ const chat = await whatsapp_chat_model_1.default.findById(chatId);
1569
+ if (!chat)
1570
+ return reply.status(404).send({ error: 'Chat not found' });
1571
+ const [messages, total] = await Promise.all([
1572
+ whatsapp_message_model_1.default.find({ chat_id: chatId })
1573
+ .sort({ createdAt: 1 })
1574
+ .skip(skip)
1575
+ .limit(limit)
1576
+ .lean(),
1577
+ whatsapp_message_model_1.default.countDocuments({ chat_id: chatId }),
1578
+ ]);
1579
+ return { messages, total, page, totalPages: Math.ceil(total / limit) };
1580
+ });
1581
+ // GET /admin/whatsapp/chats/:chatId/sessions — session history for a chat
1582
+ fastify.get('/whatsapp/chats/:chatId/sessions', async (request, reply) => {
1583
+ const { chatId } = request.params;
1584
+ const page = parseInt(request.query.page || '1', 10);
1585
+ const limit = Math.min(parseInt(request.query.limit || '20', 10), 50);
1586
+ const skip = (page - 1) * limit;
1587
+ const chat = await whatsapp_chat_model_1.default.findById(chatId);
1588
+ if (!chat)
1589
+ return reply.status(404).send({ error: 'Chat not found' });
1590
+ const [sessions, total] = await Promise.all([
1591
+ whatsapp_session_model_1.default.find({ chat_id: chatId })
1592
+ .populate('taken_over_by', 'name email')
1593
+ .sort({ started_at: -1 })
1594
+ .skip(skip)
1595
+ .limit(limit)
1596
+ .lean(),
1597
+ whatsapp_session_model_1.default.countDocuments({ chat_id: chatId }),
1598
+ ]);
1599
+ return { sessions, total, page, totalPages: Math.ceil(total / limit) };
1600
+ });
1601
+ // DELETE /admin/whatsapp/chats/:chatId — HARD delete chat + messages + sessions
1602
+ fastify.delete('/whatsapp/chats/:chatId', async (request, reply) => {
1603
+ const { chatId } = request.params;
1604
+ const chat = await whatsapp_chat_model_1.default.findById(chatId);
1605
+ if (!chat)
1606
+ return reply.status(404).send({ error: 'Chat not found' });
1607
+ // 1. Close any active WS connection and cancel timers
1608
+ if (chat.unipile_chat_id) {
1609
+ whatsapp_agent_service_1.default.closeConnection(chat.unipile_chat_id);
1610
+ }
1611
+ if (chat.active_session_id) {
1612
+ whatsapp_agent_service_1.default.cancelSession(chat.active_session_id.toString());
1613
+ }
1614
+ // 2. Hard-delete all related documents
1615
+ const [msgResult, sessResult] = await Promise.all([
1616
+ whatsapp_message_model_1.default.deleteMany({ chat_id: chatId }),
1617
+ whatsapp_session_model_1.default.deleteMany({ chat_id: chatId }),
1618
+ ]);
1619
+ // 3. Delete linked contact profile
1620
+ let profileDeleted = 0;
1621
+ if (chat.contact_profile_id) {
1622
+ const res = await whatsapp_contact_profile_model_1.default.deleteOne({ _id: chat.contact_profile_id });
1623
+ profileDeleted = res.deletedCount;
1624
+ }
1625
+ // 4. Hard-delete the chat itself
1626
+ await whatsapp_chat_model_1.default.deleteOne({ _id: chatId });
1627
+ return {
1628
+ deleted: {
1629
+ chat: 1,
1630
+ messages: msgResult.deletedCount,
1631
+ sessions: sessResult.deletedCount,
1632
+ contactProfile: profileDeleted,
1633
+ },
1634
+ };
1635
+ });
1636
+ // PUT /admin/whatsapp/grace-period — set grace period for a tenant (no minimum, for debugging)
1637
+ fastify.put('/whatsapp/grace-period', async (request, reply) => {
1638
+ const body = request.body;
1639
+ const tenant_id = body?.tenant_id ? String(body.tenant_id) : null;
1640
+ const grace_period_seconds = body?.grace_period_seconds;
1641
+ if (!tenant_id || grace_period_seconds === undefined || grace_period_seconds === null) {
1642
+ return reply.status(400).send({ error: 'tenant_id and grace_period_seconds required' });
1643
+ }
1644
+ const clamped = Math.max(1, Math.min(1800, Math.floor(grace_period_seconds)));
1645
+ const tenant = await tenant_model_1.default.findByIdAndUpdate(new mongoose_1.default.Types.ObjectId(tenant_id), { $set: { 'settings.whatsapp_agent.grace_period_seconds': clamped } }, { new: true });
1646
+ if (!tenant)
1647
+ return reply.status(404).send({ error: 'Tenant not found' });
1648
+ return {
1649
+ tenant_id: String(tenant._id),
1650
+ grace_period_seconds: tenant.settings?.whatsapp_agent?.grace_period_seconds ?? clamped,
1651
+ };
1652
+ });
1653
+ // POST /admin/whatsapp/chats/:chatId/force-resolve — force-end active session (debug)
1654
+ fastify.post('/whatsapp/chats/:chatId/force-resolve', async (request, reply) => {
1655
+ const { chatId } = request.params;
1656
+ const chat = await whatsapp_chat_model_1.default.findById(chatId);
1657
+ if (!chat)
1658
+ return reply.status(404).send({ error: 'Chat not found' });
1659
+ if (!chat.active_session_id) {
1660
+ return reply.status(400).send({ error: 'No active session' });
1661
+ }
1662
+ const session = await whatsapp_session_model_1.default.findById(chat.active_session_id);
1663
+ if (!session || session.status === 'resolved') {
1664
+ chat.active_session_id = null;
1665
+ await chat.save();
1666
+ return reply.status(400).send({ error: 'Session already resolved' });
1667
+ }
1668
+ // Cancel timers (grace period + message buffer)
1669
+ whatsapp_agent_service_1.default.cancelSession(session._id.toString());
1670
+ // Close ElevenLabs WebSocket connection
1671
+ whatsapp_agent_service_1.default.closeConnection(chat.unipile_chat_id);
1672
+ // Resolve the session
1673
+ session.status = 'resolved';
1674
+ session.resolved_by = 'manual';
1675
+ session.resolved_at = new Date();
1676
+ await session.save();
1677
+ // Clear active session from chat
1678
+ chat.active_session_id = null;
1679
+ await chat.save();
1680
+ // Generate AI profile summary + labels from the session transcript
1681
+ whatsapp_agent_service_1.default.updateContactProfileSummary(session._id.toString()).catch(err => {
1682
+ fastify.log.error({ error: err.message }, 'Profile summary update failed after force-resolve');
1683
+ });
1684
+ whatsapp_agent_service_1.default.generateSessionLabels(session._id.toString()).catch(err => {
1685
+ fastify.log.error({ error: err.message }, 'Session labelling failed after force-resolve');
1686
+ });
1687
+ return {
1688
+ resolved: {
1689
+ session_id: String(session._id),
1690
+ previous_status: session.status === 'resolved' ? 'resolved' : (session.taken_over_by ? 'active (human)' : session.status),
1691
+ },
1692
+ };
1693
+ });
1694
+ // ─── Baileys Logs ─────────────────────────────────────────
1695
+ /**
1696
+ * Build accountId→tenant lookup map.
1697
+ * Maps Baileys account UUIDs to { tenant_id, tenant_name }.
1698
+ */
1699
+ async function buildAccountTenantMap() {
1700
+ const tenants = await tenant_model_1.default.find({ 'settings.whatsapp_agent.unipile_account_id': { $ne: null } }, { name: 1, 'settings.whatsapp_agent.unipile_account_id': 1 }).lean();
1701
+ const map = new Map();
1702
+ for (const t of tenants) {
1703
+ const aid = t.settings?.whatsapp_agent?.unipile_account_id;
1704
+ if (aid)
1705
+ map.set(aid, { tenant_id: String(t._id), tenant_name: t.name });
1706
+ }
1707
+ return map;
1708
+ }
1709
+ // GET /admin/baileys/raw-messages
1710
+ fastify.get('/baileys/raw-messages', async (request) => {
1711
+ const page = parseInt(request.query.page || '1', 10);
1712
+ const limit = Math.min(parseInt(request.query.limit || '30', 10), 100);
1713
+ const skip = (page - 1) * limit;
1714
+ const db = mongoose_1.default.connection.db;
1715
+ if (!db)
1716
+ return { logs: [], total: 0, page, totalPages: 0 };
1717
+ const accountMap = await buildAccountTenantMap();
1718
+ const filter = {};
1719
+ // Filter by tenant → find matching accountId
1720
+ if (request.query.tenant_id) {
1721
+ const accountIds = [];
1722
+ for (const [aid, info] of accountMap) {
1723
+ if (info.tenant_id === request.query.tenant_id)
1724
+ accountIds.push(aid);
1725
+ }
1726
+ filter.accountId = { $in: accountIds };
1727
+ }
1728
+ if (request.query.from_me === 'true')
1729
+ filter['key.fromMe'] = true;
1730
+ else if (request.query.from_me === 'false')
1731
+ filter['key.fromMe'] = false;
1732
+ if (request.query.date_from || request.query.date_to) {
1733
+ const dateFilter = {};
1734
+ if (request.query.date_from)
1735
+ dateFilter.$gte = new Date(request.query.date_from);
1736
+ if (request.query.date_to)
1737
+ dateFilter.$lte = new Date(request.query.date_to + 'T23:59:59.999Z');
1738
+ filter.createdAt = dateFilter;
1739
+ }
1740
+ const col = db.collection('baileys_raw_messages');
1741
+ const [docs, total] = await Promise.all([
1742
+ col.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray(),
1743
+ col.countDocuments(filter),
1744
+ ]);
1745
+ const logs = docs.map(doc => ({
1746
+ ...doc,
1747
+ tenant_name: accountMap.get(doc.accountId)?.tenant_name || '-',
1748
+ tenant_id: accountMap.get(doc.accountId)?.tenant_id || null,
1749
+ }));
1750
+ return { logs, total, page, totalPages: Math.ceil(total / limit) };
1751
+ });
1752
+ // GET /admin/baileys/message-log
1753
+ fastify.get('/baileys/message-log', async (request) => {
1754
+ const page = parseInt(request.query.page || '1', 10);
1755
+ const limit = Math.min(parseInt(request.query.limit || '30', 10), 100);
1756
+ const skip = (page - 1) * limit;
1757
+ const db = mongoose_1.default.connection.db;
1758
+ if (!db)
1759
+ return { logs: [], total: 0, page, totalPages: 0 };
1760
+ const accountMap = await buildAccountTenantMap();
1761
+ const filter = {};
1762
+ if (request.query.tenant_id) {
1763
+ const accountIds = [];
1764
+ for (const [aid, info] of accountMap) {
1765
+ if (info.tenant_id === request.query.tenant_id)
1766
+ accountIds.push(aid);
1767
+ }
1768
+ filter.accountId = { $in: accountIds };
1769
+ }
1770
+ if (request.query.action)
1771
+ filter.action = request.query.action;
1772
+ if (request.query.reason)
1773
+ filter.reason = { $regex: request.query.reason, $options: 'i' };
1774
+ if (request.query.date_from || request.query.date_to) {
1775
+ const dateFilter = {};
1776
+ if (request.query.date_from)
1777
+ dateFilter.$gte = new Date(request.query.date_from);
1778
+ if (request.query.date_to)
1779
+ dateFilter.$lte = new Date(request.query.date_to + 'T23:59:59.999Z');
1780
+ filter.createdAt = dateFilter;
1781
+ }
1782
+ const col = db.collection('baileys_message_log');
1783
+ const [docs, total] = await Promise.all([
1784
+ col.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).toArray(),
1785
+ col.countDocuments(filter),
1786
+ ]);
1787
+ const logs = docs.map(doc => ({
1788
+ ...doc,
1789
+ tenant_name: accountMap.get(doc.accountId)?.tenant_name || '-',
1790
+ tenant_id: accountMap.get(doc.accountId)?.tenant_id || null,
1791
+ }));
1792
+ return { logs, total, page, totalPages: Math.ceil(total / limit) };
1793
+ });
1794
+ // GET /admin/baileys/message-log/stats — quick summary of action counts
1795
+ fastify.get('/baileys/message-log/stats', async () => {
1796
+ const db = mongoose_1.default.connection.db;
1797
+ if (!db)
1798
+ return { stats: {} };
1799
+ const col = db.collection('baileys_message_log');
1800
+ const pipeline = [
1801
+ { $group: { _id: '$action', count: { $sum: 1 } } },
1802
+ { $sort: { count: -1 } },
1803
+ ];
1804
+ const results = await col.aggregate(pipeline).toArray();
1805
+ const stats = {};
1806
+ for (const r of results)
1807
+ stats[r._id] = r.count;
1808
+ return { stats };
1809
+ });
1810
+ // ─── ElevenLabs Credit Analytics ────────────────────────────
1811
+ // GET /admin/elevenlabs/credits — cross-tenant credit usage analytics
1812
+ fastify.get('/elevenlabs/credits', async (request, reply) => {
1813
+ const now = new Date();
1814
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
1815
+ const from = request.query.from ? new Date(request.query.from) : monthStart;
1816
+ const to = request.query.to ? new Date(request.query.to) : now;
1817
+ if (isNaN(from.getTime()) || isNaN(to.getTime())) {
1818
+ return reply.status(400).send({ error: 'Invalid date format for from/to' });
1819
+ }
1820
+ const tenantFilter = request.query.tenant_id
1821
+ ? { tenant_id: new mongoose_1.default.Types.ObjectId(request.query.tenant_id) }
1822
+ : {};
1823
+ const [waPerTenant, callPerTenant, waDailyRows, callDailyRows, tenantList, waDailyPerTenantRows, callDailyPerTenantRows] = await Promise.all([
1824
+ // Q1: WA credits per tenant
1825
+ whatsapp_session_model_1.default.aggregate([
1826
+ { $match: { started_at: { $gte: from, $lte: to }, elevenlabs_cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1827
+ { $group: { _id: '$tenant_id', wa_credits: { $sum: '$elevenlabs_cost' }, wa_session_count: { $sum: 1 } } },
1828
+ ]),
1829
+ // Q2: Call credits per tenant
1830
+ call_model_1.default.aggregate([
1831
+ { $match: { createdAt: { $gte: from, $lte: to }, cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1832
+ { $group: { _id: '$tenant_id', call_credits: { $sum: '$cost' }, call_count: { $sum: 1 } } },
1833
+ ]),
1834
+ // Q3: WA daily trend
1835
+ whatsapp_session_model_1.default.aggregate([
1836
+ { $match: { started_at: { $gte: from, $lte: to }, elevenlabs_cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1837
+ { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$started_at' } }, wa: { $sum: '$elevenlabs_cost' } } },
1838
+ { $sort: { _id: 1 } },
1839
+ ]),
1840
+ // Q4: Call daily trend
1841
+ call_model_1.default.aggregate([
1842
+ { $match: { createdAt: { $gte: from, $lte: to }, cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1843
+ { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, calls: { $sum: '$cost' } } },
1844
+ { $sort: { _id: 1 } },
1845
+ ]),
1846
+ // Q5: Tenant list for dropdown
1847
+ tenant_model_1.default.find({}).select('name').lean(),
1848
+ // Q6: WA daily trend per tenant
1849
+ whatsapp_session_model_1.default.aggregate([
1850
+ { $match: { started_at: { $gte: from, $lte: to }, elevenlabs_cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1851
+ { $group: { _id: { date: { $dateToString: { format: '%Y-%m-%d', date: '$started_at' } }, tenant_id: '$tenant_id' }, credits: { $sum: '$elevenlabs_cost' } } },
1852
+ { $sort: { '_id.date': 1 } },
1853
+ ]),
1854
+ // Q7: Call daily trend per tenant
1855
+ call_model_1.default.aggregate([
1856
+ { $match: { createdAt: { $gte: from, $lte: to }, cost: { $ne: null, $gt: 0 }, ...tenantFilter } },
1857
+ { $group: { _id: { date: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, tenant_id: '$tenant_id' }, credits: { $sum: '$cost' } } },
1858
+ { $sort: { '_id.date': 1 } },
1859
+ ]),
1860
+ ]);
1861
+ // Build tenant name map
1862
+ const tenantMap = new Map();
1863
+ for (const t of tenantList)
1864
+ tenantMap.set(String(t._id), t.name);
1865
+ // Merge per-tenant results
1866
+ const perTenantMap = new Map();
1867
+ for (const row of waPerTenant) {
1868
+ const tid = String(row._id);
1869
+ perTenantMap.set(tid, {
1870
+ tenant_id: tid, tenant_name: tenantMap.get(tid) || 'Bilinmiyor',
1871
+ wa_credits: row.wa_credits, call_credits: 0, total_credits: row.wa_credits,
1872
+ wa_session_count: row.wa_session_count, call_count: 0,
1873
+ });
1874
+ }
1875
+ for (const row of callPerTenant) {
1876
+ const tid = String(row._id);
1877
+ const existing = perTenantMap.get(tid);
1878
+ if (existing) {
1879
+ existing.call_credits = row.call_credits;
1880
+ existing.call_count = row.call_count;
1881
+ existing.total_credits = existing.wa_credits + row.call_credits;
1882
+ }
1883
+ else {
1884
+ perTenantMap.set(tid, {
1885
+ tenant_id: tid, tenant_name: tenantMap.get(tid) || 'Bilinmiyor',
1886
+ wa_credits: 0, call_credits: row.call_credits, total_credits: row.call_credits,
1887
+ wa_session_count: 0, call_count: row.call_count,
1888
+ });
1889
+ }
1890
+ }
1891
+ const per_tenant = [...perTenantMap.values()].sort((a, b) => b.total_credits - a.total_credits);
1892
+ // Summary from per_tenant
1893
+ const summary = per_tenant.reduce((acc, t) => ({
1894
+ total_credits: acc.total_credits + t.total_credits,
1895
+ wa_credits: acc.wa_credits + t.wa_credits,
1896
+ call_credits: acc.call_credits + t.call_credits,
1897
+ wa_session_count: acc.wa_session_count + t.wa_session_count,
1898
+ call_count: acc.call_count + t.call_count,
1899
+ }), { total_credits: 0, wa_credits: 0, call_credits: 0, wa_session_count: 0, call_count: 0 });
1900
+ // Fill daily trend with zeros for missing days
1901
+ const waMap = new Map(waDailyRows.map((r) => [r._id, r.wa]));
1902
+ const callMap = new Map(callDailyRows.map((r) => [r._id, r.calls]));
1903
+ const daily_trend = [];
1904
+ const cursor = new Date(from);
1905
+ cursor.setHours(0, 0, 0, 0);
1906
+ const endDate = new Date(to);
1907
+ endDate.setHours(0, 0, 0, 0);
1908
+ while (cursor <= endDate) {
1909
+ const key = cursor.toISOString().slice(0, 10);
1910
+ daily_trend.push({ date: key, wa: waMap.get(key) || 0, calls: callMap.get(key) || 0 });
1911
+ cursor.setDate(cursor.getDate() + 1);
1912
+ }
1913
+ // Build per-tenant daily trends (one key per tenant per day)
1914
+ // Result: array of { date, [tenantName]: credits, ... } for recharts multi-line
1915
+ const activeTenantIds = new Set();
1916
+ const waPTMap = new Map(); // date -> tenantId -> credits
1917
+ const callPTMap = new Map();
1918
+ for (const row of waDailyPerTenantRows) {
1919
+ const { date, tenant_id } = row._id;
1920
+ const tid = String(tenant_id);
1921
+ activeTenantIds.add(tid);
1922
+ if (!waPTMap.has(date))
1923
+ waPTMap.set(date, new Map());
1924
+ waPTMap.get(date).set(tid, row.credits);
1925
+ }
1926
+ for (const row of callDailyPerTenantRows) {
1927
+ const { date, tenant_id } = row._id;
1928
+ const tid = String(tenant_id);
1929
+ activeTenantIds.add(tid);
1930
+ if (!callPTMap.has(date))
1931
+ callPTMap.set(date, new Map());
1932
+ callPTMap.get(date).set(tid, row.credits);
1933
+ }
1934
+ // Active tenants with names (sorted by total credits)
1935
+ const activeTenants = per_tenant
1936
+ .filter(t => activeTenantIds.has(t.tenant_id))
1937
+ .map(t => ({ id: t.tenant_id, name: t.tenant_name }));
1938
+ // Fill daily per-tenant data
1939
+ const wa_daily_per_tenant = [];
1940
+ const call_daily_per_tenant = [];
1941
+ const c2 = new Date(from);
1942
+ c2.setHours(0, 0, 0, 0);
1943
+ while (c2 <= endDate) {
1944
+ const key = c2.toISOString().slice(0, 10);
1945
+ const waDay = { date: key };
1946
+ const callDay = { date: key };
1947
+ for (const t of activeTenants) {
1948
+ waDay[t.name] = waPTMap.get(key)?.get(t.id) || 0;
1949
+ callDay[t.name] = callPTMap.get(key)?.get(t.id) || 0;
1950
+ }
1951
+ wa_daily_per_tenant.push(waDay);
1952
+ call_daily_per_tenant.push(callDay);
1953
+ c2.setDate(c2.getDate() + 1);
1954
+ }
1955
+ return {
1956
+ summary,
1957
+ daily_trend,
1958
+ per_tenant,
1959
+ wa_daily_per_tenant,
1960
+ call_daily_per_tenant,
1961
+ active_tenants: activeTenants,
1962
+ tenants: tenantList.map(t => ({ _id: String(t._id), name: t.name })),
1963
+ };
1964
+ });
1965
+ }
1966
+ //# sourceMappingURL=admin.routes.js.map