rez_core 3.1.110 → 3.1.111

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 (381) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.idea/250218_nodejs_core.iml +9 -0
  3. package/.idea/codeStyles/Project.xml +59 -0
  4. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  5. package/.idea/copilot.data.migration.agent.xml +6 -0
  6. package/.idea/copilot.data.migration.ask.xml +6 -0
  7. package/.idea/copilot.data.migration.ask2agent.xml +6 -0
  8. package/.idea/copilot.data.migration.edit.xml +6 -0
  9. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  10. package/.idea/misc.xml +6 -0
  11. package/.idea/modules.xml +8 -0
  12. package/.idea/prettier.xml +6 -0
  13. package/.idea/vcs.xml +6 -0
  14. package/.prettierrc +3 -3
  15. package/README.md +99 -99
  16. package/dist/module/auth/guards/role.guard.js +3 -3
  17. package/dist/module/auth/services/auth.service.js +2 -2
  18. package/dist/module/filter/repository/saved-filter.repository.js +4 -4
  19. package/dist/module/filter/service/filter-evaluator.service.js +2 -2
  20. package/dist/module/filter/service/filter.service.js +7 -7
  21. package/dist/module/integration/examples/usage.example.js +9 -9
  22. package/dist/module/integration/service/integration.service.js +7 -6
  23. package/dist/module/integration/service/integration.service.js.map +1 -1
  24. package/dist/module/integration/service/wrapper.service.js +17 -17
  25. package/dist/module/listmaster/service/list-master-item.service.js +2 -2
  26. package/dist/module/listmaster/service/list-master.service.js +2 -2
  27. package/dist/module/mapper/service/field-mapper.service.js +4 -4
  28. package/dist/module/mapper/service/mapper.service.js +2 -2
  29. package/dist/module/meta/service/entity-dynamic.service.js +18 -18
  30. package/dist/module/meta/service/entity-list.service.js +3 -3
  31. package/dist/module/meta/service/entity-master.service.js +3 -3
  32. package/dist/module/meta/service/entity-relation.service.js +11 -11
  33. package/dist/module/meta/service/entity-service-impl.service.js +3 -3
  34. package/dist/module/meta/service/resolver.service.js +3 -3
  35. package/dist/module/module/repository/menu.repository.js +12 -12
  36. package/dist/module/notification/service/notification.service.js +12 -12
  37. package/dist/module/user/controller/login.controller.js +18 -18
  38. package/dist/module/user/service/role.service.js +4 -4
  39. package/dist/module/user/service/user-session.service.js +2 -2
  40. package/dist/module/workflow/repository/action.repository.js +16 -16
  41. package/dist/module/workflow/repository/comm-template.repository.js +6 -6
  42. package/dist/module/workflow/repository/form-master.repository.js +2 -2
  43. package/dist/module/workflow/repository/stage-group.repository.js +23 -23
  44. package/dist/module/workflow/repository/stage-movement.repository.js +11 -11
  45. package/dist/module/workflow/repository/stage.repository.js +8 -8
  46. package/dist/module/workflow/service/action-template-mapping.service.js +10 -10
  47. package/dist/module/workflow/service/action.service.js +7 -7
  48. package/dist/module/workflow/service/entity-modification.service.js +6 -6
  49. package/dist/module/workflow/service/stage-group.service.js +5 -5
  50. package/dist/module/workflow/service/stage.service.js +2 -2
  51. package/dist/module/workflow/service/task.service.js +33 -33
  52. package/dist/module/workflow/service/workflow-list-master.service.js +15 -15
  53. package/dist/module/workflow/service/workflow-meta.service.js +50 -50
  54. package/dist/module/workflow/service/workflow.service.js +2 -2
  55. package/dist/tsconfig.build.tsbuildinfo +1 -1
  56. package/dist/utils/service/reflection-helper.service.js +2 -2
  57. package/docs/modules/event-driven-integration-design.md +91 -91
  58. package/docs/modules/integration.md +250 -250
  59. package/eslint.config.mjs +34 -34
  60. package/nest-cli.json +14 -14
  61. package/package.json +118 -118
  62. package/server.log +850 -0
  63. package/src/app.controller.ts +12 -12
  64. package/src/app.module.ts +49 -49
  65. package/src/app.service.ts +8 -8
  66. package/src/config/config.module.ts +18 -18
  67. package/src/config/database.config.ts +23 -23
  68. package/src/constant/global.constant.ts +67 -67
  69. package/src/core.module.ts +81 -81
  70. package/src/decorators/roles.decorator.ts +7 -7
  71. package/src/dtos/response.dto.ts +6 -6
  72. package/src/dtos/response.ts +5 -5
  73. package/src/index.ts +1 -1
  74. package/src/module/auth/auth.module.ts +49 -49
  75. package/src/module/auth/controller/auth.controller.ts +28 -28
  76. package/src/module/auth/guards/google-auth.guard.ts +9 -9
  77. package/src/module/auth/guards/jwt.guard.ts +22 -22
  78. package/src/module/auth/guards/role.guard.ts +68 -68
  79. package/src/module/auth/services/auth.service.ts +50 -50
  80. package/src/module/auth/services/jwt.service.ts +11 -11
  81. package/src/module/auth/strategies/google.strategy.ts +54 -54
  82. package/src/module/auth/strategies/jwt.strategy.ts +58 -58
  83. package/src/module/auth/strategies/local.strategy.ts +13 -13
  84. package/src/module/dashboard/controller/dashboard.controller.ts +36 -36
  85. package/src/module/dashboard/dashboard.module.ts +21 -21
  86. package/src/module/dashboard/entity/dashboard_page_data.entity.ts +27 -27
  87. package/src/module/dashboard/entity/widget_master.entity.ts +18 -18
  88. package/src/module/dashboard/repository/dashboard.repository.ts +42 -42
  89. package/src/module/dashboard/service/dashboard.service.ts +73 -73
  90. package/src/module/dev/dev.module.ts +12 -12
  91. package/src/module/dev/service/dev.service.ts +7 -7
  92. package/src/module/enterprise/controller/organization.controller.ts +36 -36
  93. package/src/module/enterprise/enterprise.module.ts +30 -30
  94. package/src/module/enterprise/entity/enterprise.entity.ts +37 -37
  95. package/src/module/enterprise/entity/organization-app-mapping.entity.ts +13 -13
  96. package/src/module/enterprise/entity/organization.entity.ts +92 -92
  97. package/src/module/enterprise/repository/enterprise.repository.ts +31 -31
  98. package/src/module/enterprise/repository/organization.repository.ts +26 -26
  99. package/src/module/enterprise/repository/school.repository.ts +282 -282
  100. package/src/module/enterprise/service/brand.service.ts +5 -5
  101. package/src/module/enterprise/service/enterprise.service.ts +16 -16
  102. package/src/module/enterprise/service/organization-app-mapping.service.ts +4 -4
  103. package/src/module/enterprise/service/organization.service.ts +145 -145
  104. package/src/module/filter/controller/filter.controller.ts +84 -84
  105. package/src/module/filter/dto/filter-request.dto.ts +38 -38
  106. package/src/module/filter/entity/saved-filter-detail.entity.ts +41 -41
  107. package/src/module/filter/entity/saved-filter-master.entity.ts +23 -23
  108. package/src/module/filter/filter.module.ts +31 -31
  109. package/src/module/filter/repository/saved-filter.repository.ts +168 -168
  110. package/src/module/filter/service/filter-evaluator.service.ts +86 -86
  111. package/src/module/filter/service/filter.service.ts +841 -841
  112. package/src/module/filter/service/saved-filter.service.ts +170 -170
  113. package/src/module/ics/controller/ics.controller.ts +21 -21
  114. package/src/module/ics/dto/ics.dto.ts +55 -55
  115. package/src/module/ics/ics.module.ts +13 -13
  116. package/src/module/ics/service/ics.service.ts +60 -60
  117. package/src/module/integration/controller/calender-event.controller.ts +31 -31
  118. package/src/module/integration/controller/integration.controller.ts +662 -662
  119. package/src/module/integration/controller/wrapper.controller.ts +39 -39
  120. package/src/module/integration/dto/create-config.dto.ts +526 -526
  121. package/src/module/integration/entity/integration-config.entity.ts +112 -112
  122. package/src/module/integration/entity/integration-entity-mapper.entity.ts +14 -14
  123. package/src/module/integration/entity/integration-source.entity.ts +17 -17
  124. package/src/module/integration/entity/user-integration.entity.ts +70 -70
  125. package/src/module/integration/examples/usage.example.ts +338 -338
  126. package/src/module/integration/factories/base.factory.ts +7 -7
  127. package/src/module/integration/factories/email.factory.ts +49 -49
  128. package/src/module/integration/factories/integration.factory.ts +121 -121
  129. package/src/module/integration/factories/sms.factory.ts +51 -51
  130. package/src/module/integration/factories/telephone.factory.ts +41 -41
  131. package/src/module/integration/factories/whatsapp.factory.ts +56 -56
  132. package/src/module/integration/integration.module.ts +110 -110
  133. package/src/module/integration/service/calendar-event.service.ts +118 -118
  134. package/src/module/integration/service/integration-entity-mapper.service.ts +17 -17
  135. package/src/module/integration/service/integration-queue.service.ts +229 -229
  136. package/src/module/integration/service/integration.service.ts +2527 -2534
  137. package/src/module/integration/service/oauth.service.ts +224 -224
  138. package/src/module/integration/service/wrapper.service.ts +373 -373
  139. package/src/module/integration/strategies/email/gmail-api.strategy.ts +287 -287
  140. package/src/module/integration/strategies/email/outlook-api.strategy.ts +44 -44
  141. package/src/module/integration/strategies/email/outlook.strategy.ts +64 -64
  142. package/src/module/integration/strategies/email/sendgrid-api.strategy.ts +261 -261
  143. package/src/module/integration/strategies/integration.strategy.ts +96 -96
  144. package/src/module/integration/strategies/sms/gupshup-sms.strategy.ts +146 -146
  145. package/src/module/integration/strategies/sms/msg91-sms.strategy.ts +164 -164
  146. package/src/module/integration/strategies/sms/tubelight-sms.strategy.ts +163 -163
  147. package/src/module/integration/strategies/telephone/ozonetel-voice.strategy.ts +237 -237
  148. package/src/module/integration/strategies/telephone/tubelight-voice.strategy.ts +209 -209
  149. package/src/module/integration/strategies/whatsapp/gupshup-whatsapp.strategy.ts +359 -359
  150. package/src/module/integration/strategies/whatsapp/tubelight-whatsapp.strategy.ts +371 -371
  151. package/src/module/integration/strategies/whatsapp/whatsapp-cloud.strategy.ts +403 -403
  152. package/src/module/integration/strategies/whatsapp/whatsapp.strategy.ts +57 -57
  153. package/src/module/layout/controller/layout.controller.ts +47 -47
  154. package/src/module/layout/entity/header-items.entity.ts +28 -28
  155. package/src/module/layout/entity/header-section.entity.ts +19 -19
  156. package/src/module/layout/layout.module.ts +21 -21
  157. package/src/module/layout/repository/header-items.repository.ts +18 -18
  158. package/src/module/layout/repository/header-section.repository.ts +22 -22
  159. package/src/module/layout/service/header-section.service.ts +25 -25
  160. package/src/module/layout_preference/controller/layout_preference.controller.ts +47 -47
  161. package/src/module/layout_preference/entity/layout_preference.entity.ts +28 -28
  162. package/src/module/layout_preference/layout_preference.module.ts +18 -18
  163. package/src/module/layout_preference/repository/layout_preference.repository.ts +30 -30
  164. package/src/module/layout_preference/service/layout_preference.service.ts +140 -140
  165. package/src/module/lead/controller/lead.controller.ts +30 -30
  166. package/src/module/lead/lead.module.ts +14 -14
  167. package/src/module/lead/repository/lead.repository.ts +41 -41
  168. package/src/module/lead/service/lead.service.ts +54 -54
  169. package/src/module/listmaster/controller/list-master.controller.ts +143 -143
  170. package/src/module/listmaster/entity/list-master-items.entity.ts +41 -41
  171. package/src/module/listmaster/entity/list-master.entity.ts +32 -32
  172. package/src/module/listmaster/listmaster.module.ts +30 -30
  173. package/src/module/listmaster/repository/list-master-items.repository.ts +169 -169
  174. package/src/module/listmaster/repository/list-master.repository.ts +46 -46
  175. package/src/module/listmaster/service/list-master-item.service.ts +292 -292
  176. package/src/module/listmaster/service/list-master.service.ts +360 -360
  177. package/src/module/mapper/controller/field-mapper.controller.ts +69 -69
  178. package/src/module/mapper/controller/mapper.controller.ts +12 -12
  179. package/src/module/mapper/dto/field-mapper.dto.ts +14 -14
  180. package/src/module/mapper/entity/field-lovs.entity.ts +20 -20
  181. package/src/module/mapper/entity/field-mapper.entity.ts +37 -37
  182. package/src/module/mapper/entity/mapper.entity.ts +17 -17
  183. package/src/module/mapper/mapper.module.ts +34 -34
  184. package/src/module/mapper/repository/field-lovs.repository.ts +35 -35
  185. package/src/module/mapper/repository/field-mapper.repository.ts +42 -42
  186. package/src/module/mapper/repository/mapper.repository.ts +14 -14
  187. package/src/module/mapper/service/field-mapper.service.ts +223 -223
  188. package/src/module/mapper/service/mapper.service.ts +72 -72
  189. package/src/module/master/controller/master.controller.ts +74 -74
  190. package/src/module/master/service/master.service.ts +486 -486
  191. package/src/module/meta/controller/app-master.controller.ts +38 -38
  192. package/src/module/meta/controller/attribute-master.controller.ts +66 -66
  193. package/src/module/meta/controller/entity-dynamic.controller.ts +111 -111
  194. package/src/module/meta/controller/entity-master.controller.ts +28 -28
  195. package/src/module/meta/controller/entity-relation.controller.ts +36 -36
  196. package/src/module/meta/controller/entity.controller.ts +385 -385
  197. package/src/module/meta/controller/media.controller.ts +82 -82
  198. package/src/module/meta/controller/meta.controller.ts +96 -96
  199. package/src/module/meta/controller/view-master.controller.ts +86 -86
  200. package/src/module/meta/dto/entity-list-data.dto.ts +6 -6
  201. package/src/module/meta/dto/entity-tab.dto.ts +4 -4
  202. package/src/module/meta/dto/entity-table.dto.ts +9 -9
  203. package/src/module/meta/entity/app-master.entity.ts +34 -34
  204. package/src/module/meta/entity/attribute-master.entity.ts +87 -87
  205. package/src/module/meta/entity/base-entity.entity.ts +75 -75
  206. package/src/module/meta/entity/entity-master.entity.ts +78 -78
  207. package/src/module/meta/entity/entity-relation-data.entity.ts +29 -29
  208. package/src/module/meta/entity/entity-relation.entity.ts +23 -23
  209. package/src/module/meta/entity/entity-table-column.entity.ts +61 -61
  210. package/src/module/meta/entity/entity-table.entity.ts +50 -50
  211. package/src/module/meta/entity/media-data.entity.ts +32 -32
  212. package/src/module/meta/entity/preference.entity.ts +62 -62
  213. package/src/module/meta/entity/view-master.entity.ts +41 -41
  214. package/src/module/meta/entity.module.ts +156 -156
  215. package/src/module/meta/repository/app-master.repository.ts +20 -20
  216. package/src/module/meta/repository/attribute-master.repository.ts +110 -110
  217. package/src/module/meta/repository/entity-master.repository.ts +61 -61
  218. package/src/module/meta/repository/entity-table-column.repository.ts +39 -39
  219. package/src/module/meta/repository/entity-table.repository.ts +53 -53
  220. package/src/module/meta/repository/media-data.repository.ts +50 -50
  221. package/src/module/meta/repository/preference.repository.ts +20 -20
  222. package/src/module/meta/repository/user-app-mapping.repository.ts +28 -28
  223. package/src/module/meta/repository/view-master.repository.ts +42 -42
  224. package/src/module/meta/service/app-master.service.ts +37 -37
  225. package/src/module/meta/service/attribute-master.service.ts +117 -117
  226. package/src/module/meta/service/common.service.ts +9 -9
  227. package/src/module/meta/service/entity-dynamic.service.ts +718 -718
  228. package/src/module/meta/service/entity-list.service.ts +205 -205
  229. package/src/module/meta/service/entity-master.service.ts +164 -164
  230. package/src/module/meta/service/entity-realation-data.service.ts +9 -9
  231. package/src/module/meta/service/entity-relation.service.ts +69 -69
  232. package/src/module/meta/service/entity-service-impl.service.ts +513 -513
  233. package/src/module/meta/service/entity-table-column.service.ts +39 -39
  234. package/src/module/meta/service/entity-table.service.ts +150 -150
  235. package/src/module/meta/service/entity-validation.service.ts +185 -185
  236. package/src/module/meta/service/entity.service.ts +67 -67
  237. package/src/module/meta/service/field-group.service.ts +103 -103
  238. package/src/module/meta/service/media-data.service.ts +140 -140
  239. package/src/module/meta/service/populate-meta.service.ts +153 -153
  240. package/src/module/meta/service/preference.service.ts +16 -16
  241. package/src/module/meta/service/resolver.service.ts +256 -256
  242. package/src/module/meta/service/section-master.service.ts +104 -104
  243. package/src/module/meta/service/update-form-json.service.ts +22 -22
  244. package/src/module/meta/service/user-app-mapping.service.ts +17 -17
  245. package/src/module/meta/service/view-master.service.ts +91 -91
  246. package/src/module/module/controller/menu.controller.ts +15 -15
  247. package/src/module/module/controller/module-access.controller.ts +132 -132
  248. package/src/module/module/entity/menu.entity.ts +43 -43
  249. package/src/module/module/entity/module-access.entity.ts +25 -25
  250. package/src/module/module/entity/module-action.entity.ts +17 -17
  251. package/src/module/module/entity/module.entity.ts +52 -52
  252. package/src/module/module/module.module.ts +42 -42
  253. package/src/module/module/repository/menu.repository.ts +184 -184
  254. package/src/module/module/repository/module-access.repository.ts +324 -324
  255. package/src/module/module/service/menu.service.ts +82 -82
  256. package/src/module/module/service/module-access.service.ts +209 -209
  257. package/src/module/notification/controller/notification.controller.ts +58 -58
  258. package/src/module/notification/controller/otp.controller.ts +117 -117
  259. package/src/module/notification/entity/notification.entity.ts +23 -23
  260. package/src/module/notification/entity/otp.entity.ts +28 -28
  261. package/src/module/notification/firebase-admin.config.ts +22 -22
  262. package/src/module/notification/notification.module.ts +69 -69
  263. package/src/module/notification/repository/otp.repository.ts +27 -27
  264. package/src/module/notification/service/email.service.ts +127 -127
  265. package/src/module/notification/service/notification.service.ts +130 -130
  266. package/src/module/notification/service/otp.service.ts +121 -121
  267. package/src/module/third-party-module/entity/third-party-api-registry.entity.ts +52 -52
  268. package/src/module/third-party-module/repository/third-party-api-registry.repository.ts +20 -20
  269. package/src/module/third-party-module/service/api-registry.service.ts +13 -13
  270. package/src/module/third-party-module/third-party.module.ts +12 -12
  271. package/src/module/user/controller/login.controller.ts +197 -197
  272. package/src/module/user/controller/user.controller.ts +40 -40
  273. package/src/module/user/dto/create-user.dto.ts +62 -62
  274. package/src/module/user/dto/update-user.dto.ts +4 -4
  275. package/src/module/user/entity/role.entity.ts +33 -33
  276. package/src/module/user/entity/user-role-mapping.entity.ts +38 -38
  277. package/src/module/user/entity/user-session.entity.ts +61 -61
  278. package/src/module/user/entity/user.entity.ts +68 -68
  279. package/src/module/user/repository/role.repository.ts +96 -96
  280. package/src/module/user/repository/user-role-mapping.repository.ts +126 -126
  281. package/src/module/user/repository/user.repository.ts +50 -50
  282. package/src/module/user/repository/userSession.repository.ts +33 -33
  283. package/src/module/user/service/login.service.ts +280 -280
  284. package/src/module/user/service/role.service.ts +189 -189
  285. package/src/module/user/service/user-role-mapping.service.ts +98 -98
  286. package/src/module/user/service/user-session.service.ts +168 -168
  287. package/src/module/user/service/user.service.ts +353 -353
  288. package/src/module/user/user.module.ts +65 -65
  289. package/src/module/workflow/controller/action-category.controller.ts +54 -54
  290. package/src/module/workflow/controller/action-resource-mapping.controller.ts +23 -23
  291. package/src/module/workflow/controller/action-template-mapping.controller.ts +35 -35
  292. package/src/module/workflow/controller/action.controller.ts +95 -95
  293. package/src/module/workflow/controller/activity-log.controller.ts +55 -55
  294. package/src/module/workflow/controller/comm-template.controller.ts +34 -34
  295. package/src/module/workflow/controller/entity-modification.controller.ts +35 -35
  296. package/src/module/workflow/controller/form-master.controller.ts +43 -43
  297. package/src/module/workflow/controller/stage-group.controller.ts +48 -48
  298. package/src/module/workflow/controller/stage.controller.ts +47 -47
  299. package/src/module/workflow/controller/task.controller.ts +77 -77
  300. package/src/module/workflow/controller/workflow-list-master.controller.ts +44 -44
  301. package/src/module/workflow/controller/workflow-meta.controller.ts +80 -80
  302. package/src/module/workflow/controller/workflow.controller.ts +67 -67
  303. package/src/module/workflow/entity/action-category.entity.ts +38 -38
  304. package/src/module/workflow/entity/action-data.entity.ts +55 -55
  305. package/src/module/workflow/entity/action-resources-mapping.entity.ts +29 -29
  306. package/src/module/workflow/entity/action-template-mapping.entity.ts +17 -17
  307. package/src/module/workflow/entity/action.entity.ts +50 -50
  308. package/src/module/workflow/entity/activity-log.entity.ts +43 -43
  309. package/src/module/workflow/entity/comm-template.entity.ts +43 -43
  310. package/src/module/workflow/entity/entity-modification.entity.ts +38 -38
  311. package/src/module/workflow/entity/form-master.entity.ts +27 -27
  312. package/src/module/workflow/entity/form.entity.ts +25 -25
  313. package/src/module/workflow/entity/stage-action-mapping.entity.ts +17 -17
  314. package/src/module/workflow/entity/stage-group.entity.ts +23 -23
  315. package/src/module/workflow/entity/stage-movement-data.entity.ts +38 -38
  316. package/src/module/workflow/entity/stage.entity.ts +20 -20
  317. package/src/module/workflow/entity/task-data.entity.ts +88 -88
  318. package/src/module/workflow/entity/template-attach-mapper.entity.ts +30 -30
  319. package/src/module/workflow/entity/workflow-data.entity.ts +11 -11
  320. package/src/module/workflow/entity/workflow-level-mapping.entity.ts +18 -18
  321. package/src/module/workflow/entity/workflow.entity.ts +20 -20
  322. package/src/module/workflow/repository/action-category.repository.ts +79 -79
  323. package/src/module/workflow/repository/action-data.repository.ts +219 -219
  324. package/src/module/workflow/repository/action.repository.ts +277 -277
  325. package/src/module/workflow/repository/activity-log.repository.ts +121 -121
  326. package/src/module/workflow/repository/comm-template.repository.ts +142 -142
  327. package/src/module/workflow/repository/form-master.repository.ts +61 -61
  328. package/src/module/workflow/repository/stage-group.repository.ts +176 -176
  329. package/src/module/workflow/repository/stage-movement.repository.ts +227 -227
  330. package/src/module/workflow/repository/stage.repository.ts +118 -118
  331. package/src/module/workflow/repository/task.repository.ts +113 -113
  332. package/src/module/workflow/repository/workflow.repository.ts +42 -42
  333. package/src/module/workflow/service/action-category.service.ts +33 -33
  334. package/src/module/workflow/service/action-data.service.ts +62 -62
  335. package/src/module/workflow/service/action-resources-mapping.service.ts +10 -10
  336. package/src/module/workflow/service/action-template-mapping.service.ts +55 -55
  337. package/src/module/workflow/service/action.service.ts +247 -247
  338. package/src/module/workflow/service/activity-log.service.ts +107 -107
  339. package/src/module/workflow/service/comm-template.service.ts +121 -121
  340. package/src/module/workflow/service/entity-modification.service.ts +67 -67
  341. package/src/module/workflow/service/form-master.service.ts +35 -35
  342. package/src/module/workflow/service/populate-workflow.service.ts +326 -326
  343. package/src/module/workflow/service/stage-action-mapping.service.ts +5 -5
  344. package/src/module/workflow/service/stage-group.service.ts +300 -300
  345. package/src/module/workflow/service/stage.service.ts +140 -140
  346. package/src/module/workflow/service/task.service.ts +504 -504
  347. package/src/module/workflow/service/workflow-list-master.service.ts +60 -60
  348. package/src/module/workflow/service/workflow-meta.service.ts +564 -564
  349. package/src/module/workflow/service/workflow.service.ts +205 -205
  350. package/src/module/workflow/workflow.module.ts +176 -176
  351. package/src/module/workflow-automation/controller/workflow-automation.controller.ts +21 -21
  352. package/src/module/workflow-automation/entity/workflow-automation-action.entity.ts +26 -26
  353. package/src/module/workflow-automation/entity/workflow-automation.entity.ts +35 -35
  354. package/src/module/workflow-automation/interface/action.decorator.ts +7 -7
  355. package/src/module/workflow-automation/interface/action.interface.ts +5 -5
  356. package/src/module/workflow-automation/service/action-registery.service.ts +35 -35
  357. package/src/module/workflow-automation/service/workflow-automation-engine.service.ts +214 -214
  358. package/src/module/workflow-automation/service/workflow-automation.service.ts +343 -343
  359. package/src/module/workflow-automation/workflow-automation.module.ts +34 -34
  360. package/src/resources/dev.properties.yaml +30 -30
  361. package/src/resources/local.properties.yaml +23 -23
  362. package/src/resources/properties.module.ts +12 -12
  363. package/src/resources/properties.yaml.ts +11 -11
  364. package/src/resources/uat.properties.yaml +15 -15
  365. package/src/utils/dto/excel-data.dto.ts +14 -14
  366. package/src/utils/dto/excelsheet-data.dto.ts +5 -5
  367. package/src/utils/service/base64util.service.ts +18 -18
  368. package/src/utils/service/clockIDGenUtil.service.ts +21 -21
  369. package/src/utils/service/codeGenerator.service.ts +22 -22
  370. package/src/utils/service/dateUtil.service.ts +17 -17
  371. package/src/utils/service/encryptUtil.service.ts +97 -97
  372. package/src/utils/service/excel-helper.service.ts +72 -72
  373. package/src/utils/service/excelUtil.service.ts +15 -15
  374. package/src/utils/service/file-util.service.ts +11 -11
  375. package/src/utils/service/json-util.service.ts +23 -23
  376. package/src/utils/service/loggingUtil.service.ts +34 -34
  377. package/src/utils/service/reflection-helper.service.ts +62 -62
  378. package/src/utils/service/wbsCodeGen.service.ts +8 -8
  379. package/src/utils/utils.module.ts +25 -25
  380. package/tsconfig.build.json +4 -4
  381. package/tsconfig.json +24 -24
@@ -1,2534 +1,2527 @@
1
- import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
2
- import { InjectRepository } from '@nestjs/typeorm';
3
- import { DataSource, Not, Repository } from 'typeorm';
4
- import { ConfigService } from '@nestjs/config';
5
- import { google } from 'googleapis';
6
- import { IntegrationConfig } from '../entity/integration-config.entity';
7
- import { UserIntegration } from '../entity/user-integration.entity';
8
- import { IntegrationEntityMapper } from '../entity/integration-entity-mapper.entity';
9
- import { IntegrationFactory } from '../factories/integration.factory';
10
- import { IntegrationResult } from '../strategies/integration.strategy';
11
- import { GmailApiStrategy } from '../strategies/email/gmail-api.strategy';
12
- import { SendGridApiStrategy } from '../strategies/email/sendgrid-api.strategy';
13
- import { IntegrationQueueService } from './integration-queue.service';
14
- import {
15
- BulkMessageDto,
16
- BulkCreateUserIntegrationDto,
17
- CreateUserIntegrationDto,
18
- UpdateUserIntegrationDto,
19
- } from '../dto/create-config.dto';
20
- import { FieldMapperService } from '../../mapper/service/field-mapper.service';
21
- import { COMM_TEMPLATE } from '../../../constant/global.constant';
22
- import { EntityServiceImpl } from '../../meta/service/entity-service-impl.service';
23
-
24
- export interface SendMessageDto {
25
- levelId: number;
26
- levelType: string;
27
- app_code: string;
28
- to: string;
29
- message: string;
30
- mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
31
- priority?: number;
32
- user_id?: number;
33
- }
34
-
35
- export interface GenericMessageDto {
36
- levelId: number;
37
- levelType: string;
38
- app_code: string;
39
- to: string | string[];
40
- message: string;
41
- subject?: string;
42
- type?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
43
- priority?: 'high' | 'medium' | 'low';
44
- cc?: string | string[];
45
- bcc?: string | string[];
46
- html?: string;
47
- attachments?: any[];
48
- mediaUrl?: string;
49
- templateId?: string;
50
- variables?: Record<string, any>;
51
- user_id?: number;
52
- entity_type?: string;
53
- entity_id?: number;
54
- }
55
-
56
- export interface IntegrationConfigWithConfig extends IntegrationConfig {
57
- config?: any;
58
- }
59
-
60
- interface GmailOAuthState {
61
- levelId: number;
62
- levelType: string;
63
- app_code: string;
64
- email?: string;
65
- timestamp: number;
66
- }
67
-
68
- export interface GmailSSOResult {
69
- hubId: number;
70
- configId: number;
71
- }
72
-
73
- @Injectable()
74
- export class IntegrationService {
75
- private readonly logger = new Logger(IntegrationService.name);
76
- private readonly gmailOAuthStates = new Map<string, GmailOAuthState>();
77
-
78
- constructor(
79
- @InjectRepository(IntegrationConfig)
80
- private readonly configRepository: Repository<IntegrationConfig>,
81
- @InjectRepository(UserIntegration)
82
- private readonly userIntegrationRepository: Repository<UserIntegration>,
83
- @InjectRepository(IntegrationEntityMapper)
84
- private readonly entityMapperRepository: Repository<IntegrationEntityMapper>,
85
- private readonly dataSource: DataSource,
86
- private readonly integrationFactory: IntegrationFactory,
87
- private readonly gmailApiStrategy: GmailApiStrategy,
88
- private readonly sendGridApiStrategy: SendGridApiStrategy,
89
- private readonly configService: ConfigService,
90
- @Inject('FieldMapperService')
91
- private readonly fieldMapperService: FieldMapperService,
92
- private readonly entityService: EntityServiceImpl,
93
- @Inject(forwardRef(() => IntegrationQueueService))
94
- private readonly queueService?: IntegrationQueueService,
95
- ) {}
96
-
97
- private deriveServiceType(
98
- integration_type: string,
99
- integration_provider: string,
100
- config_json: any,
101
- ): string {
102
- // All integrations use API only (no SMTP support)
103
- return 'API';
104
- }
105
-
106
- async sendMessage({
107
- levelId,
108
- levelType,
109
- app_code,
110
- to,
111
- message,
112
- mode,
113
- priority = 1,
114
- user_id,
115
- }: SendMessageDto): Promise<IntegrationResult> {
116
- try {
117
- // Get active communication configs for the level
118
- const configs = await this.getActiveConfigs(
119
- levelId,
120
- levelType,
121
- app_code,
122
- mode,
123
- );
124
-
125
- if (!configs.length) {
126
- throw new Error(
127
- `No active communication configuration found for ${levelType} ${levelId}`,
128
- );
129
- }
130
-
131
- // Sort by priority if provided
132
- const sortedConfigs = this.sortConfigsByPriority(configs, priority);
133
-
134
- // Try each config until one succeeds
135
- for (const config of sortedConfigs) {
136
- try {
137
- const result = await this.sendViaConfig(config, to, message, user_id);
138
- if (result.success) {
139
- this.logger.log(
140
- `Message sent successfully via ${config.integration_provider}`,
141
- );
142
- return result;
143
- }
144
-
145
- this.logger.warn(
146
- `Failed to send via ${config.integration_provider}: ${result.error}`,
147
- );
148
- } catch (error) {
149
- this.logger.error(
150
- `Error sending via ${config.integration_provider}:`,
151
- error.message,
152
- );
153
- continue;
154
- }
155
- }
156
-
157
- throw new Error('All communication providers failed');
158
- } catch (error) {
159
- this.logger.error('Communication service error:', error.message);
160
- throw error;
161
- }
162
- }
163
-
164
- async getActiveConfigs(
165
- levelId: number,
166
- levelType: string,
167
- app_code: string,
168
- mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
169
- ): Promise<IntegrationConfig[]> {
170
- const queryBuilder = this.configRepository
171
- .createQueryBuilder('config')
172
- .where('config.level_id = :levelId', { levelId })
173
- .andWhere('config.level_type = :levelType', { levelType })
174
- .andWhere('config.app_code = :app_code', { app_code })
175
- .andWhere('config.status = 1');
176
-
177
- if (mode) {
178
- queryBuilder.andWhere('config.integration_type = :mode', { mode });
179
- }
180
-
181
- return await queryBuilder.getMany();
182
- }
183
-
184
- async getSingleActiveConfig(
185
- levelId: number,
186
- levelType: string,
187
- app_code: string,
188
- integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
189
- ): Promise<IntegrationConfig | null> {
190
- const configs = await this.configRepository
191
- .createQueryBuilder('config')
192
- .where('config.level_id = :levelId', { levelId })
193
- .andWhere('config.level_type = :levelType', { levelType })
194
- .andWhere('config.app_code = :app_code', { app_code })
195
- .andWhere('config.integration_type = :integration_type', {
196
- integration_type,
197
- })
198
- .andWhere('config.status = 1')
199
- .orderBy('config.is_default', 'DESC')
200
- .addOrderBy('config.priority', 'ASC')
201
- .addOrderBy('config.created_at', 'DESC')
202
- .getMany();
203
-
204
- return configs.length > 0 ? configs[0] : null;
205
- }
206
-
207
- async getAllIntegrationData(
208
- loggedInUser,
209
- integration_type?: string,
210
- ): Promise<any[]> {
211
- try {
212
- const allIntegrationData = await this.dataSource.query(
213
- `SELECT *
214
- FROM cr_integration_master`,
215
- );
216
-
217
- // if entityType is provided, filter the results
218
- if (integration_type) {
219
- return allIntegrationData.filter(
220
- (data) =>
221
- data.integration_type.toLowerCase() ===
222
- integration_type.toLowerCase(),
223
- );
224
- }
225
-
226
- return allIntegrationData;
227
- } catch (error) {
228
- this.logger.error('Error fetching integration data:', error.message);
229
- return [];
230
- }
231
- }
232
-
233
- private sortConfigsByPriority(
234
- configs: IntegrationConfig[],
235
- _priority: number,
236
- ): IntegrationConfig[] {
237
- return configs.sort((a, b) => {
238
- // First sort by default (true comes first)
239
- if (a.is_default !== b.is_default) {
240
- return a.is_default ? -1 : 1;
241
- }
242
- // Then by priority (lower number = higher priority)
243
- return a.priority - b.priority;
244
- });
245
- }
246
-
247
- private async sendViaConfig(
248
- config: IntegrationConfig,
249
- to: string,
250
- message: string,
251
- user_id?: number,
252
- ): Promise<IntegrationResult> {
253
- const strategy = this.integrationFactory.create(
254
- config.integration_type,
255
- 'API', // service is deprecated, using default
256
- config.integration_provider,
257
- );
258
-
259
- // Get user integration data if user_id provided
260
- let finalConfig = config.config_json;
261
- if (user_id) {
262
- const userIntegrationData = await this.getUserIntegrationForStrategy(
263
- user_id,
264
- config.id,
265
- );
266
-
267
- if (userIntegrationData) {
268
- finalConfig = {
269
- ...config.config_json,
270
- external_user_id: userIntegrationData.external_user_id,
271
- };
272
- }
273
- }
274
-
275
- const result = await strategy.sendMessage(to, message, finalConfig);
276
-
277
- // If token was refreshed, update it in the database
278
- if (result.refreshedToken && result.success) {
279
- try {
280
- const currentConfig = config.config_json as any;
281
- const updatedConfig = {
282
- ...currentConfig,
283
- accessToken: result.refreshedToken,
284
- };
285
-
286
- await this.configRepository.update(config.id, {
287
- config_json: updatedConfig,
288
- } as any);
289
-
290
- this.logger.log(
291
- `Updated access token for ${config.integration_provider} configuration`,
292
- );
293
- } catch (error) {
294
- this.logger.warn(
295
- `Failed to update refreshed token in database: ${error.message}`,
296
- );
297
- }
298
- }
299
-
300
- return result;
301
- }
302
-
303
- async createIntegrationConfig(
304
- levelId: number,
305
- levelType: string,
306
- app_code: string,
307
- configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
308
- provider: string,
309
- integration_source_id: number,
310
- config: any,
311
- priority?: number,
312
- is_default?: boolean,
313
- ): Promise<
314
- IntegrationConfig | { authUrl: string; state: string; message: string }
315
- > {
316
- // Validate that no duplicate provider configurations exist
317
- await this.validateUniqueActiveConfig(
318
- levelId,
319
- levelType,
320
- app_code,
321
- configType,
322
- provider,
323
- );
324
-
325
- // Deactivate all other configurations of the same integration type
326
- await this.configRepository.update(
327
- {
328
- level_id: levelId,
329
- level_type: levelType,
330
- app_code: app_code,
331
- integration_type: configType,
332
- status: 1,
333
- },
334
- { status: 0 },
335
- );
336
-
337
- // Validate provider and get service type from supported combinations
338
- const supportedCombinations = await this.getSupportedCombinations();
339
- const validCombination = supportedCombinations.find(
340
- (combo) =>
341
- combo.mode === configType &&
342
- combo.provider.toLowerCase() === provider.toLowerCase(),
343
- );
344
-
345
- if (!validCombination) {
346
- throw new Error(`Unsupported combination: ${configType}/${provider}`);
347
- }
348
-
349
- const service = validCombination.service;
350
-
351
- // Check if this requires OAuth flow
352
- const requiresOAuth = this.requiresOAuthFlow(configType, provider, config);
353
-
354
- if (requiresOAuth) {
355
- // Generate OAuth URL and return it instead of creating config immediately
356
- return await this.generateOAuthUrl(
357
- levelId,
358
- levelType,
359
- app_code,
360
- configType,
361
- provider,
362
- integration_source_id,
363
- config,
364
- priority,
365
- is_default,
366
- );
367
- }
368
-
369
- // Direct config creation (non-OAuth flow)
370
- return await this.createDirectConfig(
371
- levelId,
372
- levelType,
373
- app_code,
374
- configType,
375
- provider,
376
- integration_source_id,
377
- config,
378
- priority,
379
- is_default,
380
- );
381
- }
382
-
383
- getSupportedCombinations(): {
384
- mode: string;
385
- service: string;
386
- provider: string;
387
- }[] {
388
- return this.integrationFactory.getAllSupportedCombinations();
389
- }
390
-
391
- async getLevelConfigs(
392
- levelId: number,
393
- levelType: string,
394
- filters?: {
395
- app_code?: string;
396
- integration_type?: 'WA' | 'SMS' | 'EMAIL' | 'TELEPHONE';
397
- integration_provider?: string;
398
- },
399
- ): Promise<
400
- Array<IntegrationConfig & { linkedSource?: string; configDetails?: any }>
401
- > {
402
- const where: any = { level_id: levelId, level_type: levelType };
403
-
404
- if (filters?.app_code) {
405
- where.app_code = filters.app_code;
406
- }
407
-
408
- if (filters?.integration_type) {
409
- where.integration_type = filters.integration_type;
410
- }
411
-
412
- if (filters?.integration_provider) {
413
- where.integration_provider = filters.integration_provider;
414
- }
415
-
416
- const hubs = await this.configRepository.find({
417
- where,
418
- order: { created_at: 'DESC' },
419
- });
420
-
421
- // Enhance hubs with linked source information
422
- const enhancedHubs = await Promise.all(
423
- hubs.map(async (hub) => {
424
- try {
425
- const relatedConfig = await this.configRepository.findOne({
426
- where: { id: hub.id },
427
- });
428
-
429
- if (!relatedConfig) {
430
- return {
431
- ...hub,
432
- linkedSource: 'Configuration not found',
433
- configDetails: null,
434
- };
435
- }
436
-
437
- const linkedSource = this.extractLinkedSource(
438
- hub.integration_type,
439
- hub.integration_provider,
440
- relatedConfig.config_json,
441
- );
442
-
443
- const configDetails = this.extractConfigDetails(
444
- hub.integration_type,
445
- hub.integration_provider,
446
- relatedConfig.config_json,
447
- );
448
-
449
- return {
450
- ...hub,
451
- linkedSource,
452
- configDetails,
453
- };
454
- } catch (error) {
455
- this.logger.warn(
456
- `Error extracting linked source for hub ${hub.id}:`,
457
- error.message,
458
- );
459
- return {
460
- ...hub,
461
- linkedSource: 'Error retrieving source',
462
- configDetails: null,
463
- };
464
- }
465
- }),
466
- );
467
-
468
- return enhancedHubs;
469
- }
470
-
471
- private extractLinkedSource(
472
- configType: string,
473
- provider: string,
474
- configJson: any,
475
- ): string {
476
- const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
477
-
478
- try {
479
- switch (key) {
480
- // Gmail configurations
481
- case 'email_gmail':
482
- case 'email_smtp_gmail':
483
- return configJson?.email || 'Gmail account not configured';
484
-
485
- // Outlook configurations
486
- case 'email_outlook':
487
- case 'email_smtp_outlook':
488
- return configJson?.email || 'Outlook account not configured';
489
-
490
- // WhatsApp configurations
491
- case 'wa_api_whatsapp':
492
- return configJson?.phoneNumberId
493
- ? `WhatsApp Business: ${configJson.phoneNumberId}`
494
- : 'WhatsApp not configured';
495
-
496
- // SMS configurations
497
- case 'sms_third_party_twilio':
498
- return configJson?.fromNumber
499
- ? `Twilio: ${configJson.fromNumber}`
500
- : 'Twilio number not configured';
501
-
502
- case 'sms_third_party_knowlarity':
503
- return configJson?.callerNumber
504
- ? `Knowlarity: ${configJson.callerNumber}`
505
- : 'Knowlarity number not configured';
506
-
507
- // Telephone configurations
508
- case 'telephone_third_party_knowlarity':
509
- return configJson?.callerNumber
510
- ? `Knowlarity Voice: ${configJson.callerNumber}`
511
- : 'Knowlarity voice number not configured';
512
-
513
- case 'telephone_third_party_ozonetel':
514
- return configJson?.userName
515
- ? `Ozonetel Voice: ${configJson.userName}`
516
- : 'Ozonetel voice not configured';
517
-
518
- // AWS SES configurations
519
- case 'email_aws-ses':
520
- case 'email_ses':
521
- return configJson?.fromEmail || 'AWS SES not configured';
522
-
523
- // SendGrid configurations
524
- case 'email_smtp_sendgrid':
525
- return configJson?.from || 'SendGrid not configured';
526
-
527
- // Generic SMTP configurations
528
- case 'email_smtp_custom':
529
- case 'email_smtp_generic':
530
- return configJson?.from || configJson?.user || 'SMTP not configured';
531
-
532
- default:
533
- // Generic fallback - try to find common identifier fields
534
- if (configJson?.email) return configJson.email;
535
- if (configJson?.from) return configJson.from;
536
- if (configJson?.user) return configJson.user;
537
- if (configJson?.phoneNumberId) return configJson.phoneNumberId;
538
- if (configJson?.fromNumber) return configJson.fromNumber;
539
- if (configJson?.callerNumber) return configJson.callerNumber;
540
-
541
- return `${provider} configured`;
542
- }
543
- } catch (error) {
544
- return 'Configuration error';
545
- }
546
- }
547
-
548
- private extractConfigDetails(
549
- configType: string,
550
- provider: string,
551
- configJson: any,
552
- ): any {
553
- const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
554
-
555
- try {
556
- switch (key) {
557
- // Gmail configurations
558
- case 'email_gmail':
559
- return {
560
- email: configJson?.email,
561
- authMethod: configJson?.authMethod || 'OAUTH2',
562
- hasRefreshToken: !!configJson?.refreshToken,
563
- isExpired: configJson?.expiryDate
564
- ? new Date(configJson.expiryDate) < new Date()
565
- : false,
566
- };
567
-
568
- case 'email_smtp_gmail':
569
- return {
570
- email: configJson?.email,
571
- authMethod: 'SMTP',
572
- hasPassword: !!configJson?.password,
573
- };
574
-
575
- // Outlook configurations
576
- case 'email_outlook':
577
- return {
578
- email: configJson?.email,
579
- authMethod: 'OAUTH2',
580
- hasRefreshToken: !!configJson?.refreshToken,
581
- tenantId: configJson?.tenantId,
582
- };
583
-
584
- case 'email_smtp_outlook':
585
- return {
586
- email: configJson?.email,
587
- authMethod: 'SMTP',
588
- hasPassword: !!configJson?.password,
589
- };
590
-
591
- // WhatsApp configurations
592
- case 'wa_api_whatsapp':
593
- return {
594
- phoneNumberId: configJson?.phoneNumberId,
595
- apiVersion: configJson?.apiVersion || 'v17.0',
596
- hasAccessToken: !!configJson?.accessToken,
597
- };
598
-
599
- // SMS configurations
600
- case 'sms_third_party_twilio':
601
- return {
602
- fromNumber: configJson?.fromNumber,
603
- accountSid: configJson?.accountSid
604
- ? configJson.accountSid.substring(0, 8) + '...'
605
- : null,
606
- hasAuthToken: !!configJson?.authToken,
607
- };
608
-
609
- case 'sms_third_party_knowlarity':
610
- return {
611
- callerNumber: configJson?.callerNumber,
612
- callType: configJson?.callType || 'sms',
613
- hasApiSecret: !!configJson?.apiSecret,
614
- };
615
-
616
- // Telephone configurations
617
- case 'telephone_third_party_knowlarity':
618
- return {
619
- callerNumber: configJson?.callerNumber,
620
- callType: configJson?.callType || 'voice',
621
- language: configJson?.language || 'en',
622
- hasApiSecret: !!configJson?.apiSecret,
623
- };
624
-
625
- case 'telephone_third_party_ozonetel':
626
- return {
627
- userName: configJson?.userName,
628
- agentID: configJson?.agentID,
629
- campaignName: configJson?.campaignName,
630
- agentLoginUrl: configJson?.agentLoginUrl,
631
- hasApiKey: !!configJson?.apiKey,
632
- };
633
-
634
- // AWS SES configurations
635
- case 'email_aws-ses':
636
- case 'email_ses':
637
- return {
638
- fromEmail: configJson?.fromEmail,
639
- region: configJson?.region,
640
- hasCredentials: !!(
641
- configJson?.accessKeyId && configJson?.secretAccessKey
642
- ),
643
- };
644
-
645
- // SendGrid configurations
646
- case 'email_smtp_sendgrid':
647
- return {
648
- from: configJson?.from,
649
- hasApiKey: !!configJson?.apiKey,
650
- templateId: configJson?.templateId,
651
- };
652
-
653
- // Generic SMTP configurations
654
- case 'email_smtp_custom':
655
- case 'email_smtp_generic':
656
- return {
657
- host: configJson?.host,
658
- port: configJson?.port || 587,
659
- secure: configJson?.secure || false,
660
- from: configJson?.from || configJson?.user,
661
- hasCredentials: !!(configJson?.user && configJson?.password),
662
- };
663
-
664
- default: {
665
- // Generic details - return safe subset of config
666
- const safeConfig: any = {};
667
-
668
- // Include non-sensitive fields
669
- const safeFields = [
670
- 'email',
671
- 'from',
672
- 'user',
673
- 'host',
674
- 'port',
675
- 'secure',
676
- 'phoneNumberId',
677
- 'fromNumber',
678
- 'callerNumber',
679
- 'region',
680
- 'apiVersion',
681
- 'callType',
682
- 'language',
683
- 'templateId',
684
- ];
685
-
686
- safeFields.forEach((field) => {
687
- if (configJson?.[field]) {
688
- safeConfig[field] = configJson[field];
689
- }
690
- });
691
-
692
- // Include boolean indicators for sensitive fields
693
- const sensitiveFields = [
694
- 'accessToken',
695
- 'refreshToken',
696
- 'password',
697
- 'authToken',
698
- 'apiKey',
699
- 'apiSecret',
700
- 'clientSecret',
701
- 'secretAccessKey',
702
- ];
703
-
704
- sensitiveFields.forEach((field) => {
705
- if (configJson?.[field]) {
706
- safeConfig[
707
- `has${field.charAt(0).toUpperCase() + field.slice(1)}`
708
- ] = true;
709
- }
710
- });
711
-
712
- return safeConfig;
713
- }
714
- }
715
- } catch (error) {
716
- return { error: 'Unable to extract configuration details' };
717
- }
718
- }
719
-
720
- async updateConfigStatus(hubId: number, status: number): Promise<void> {
721
- // Find the hub to get level and type information
722
- const config = await this.configRepository.findOne({
723
- where: { id: hubId },
724
- });
725
-
726
- if (!config) {
727
- throw new Error('Integration configuration not found');
728
- }
729
-
730
- // If activating, deactivate ALL other configs of the same integration type
731
- if (status === 1) {
732
- await this.configRepository.update(
733
- {
734
- level_id: config.level_id,
735
- level_type: config.level_type,
736
- app_code: config.app_code,
737
- integration_type: config.integration_type,
738
- status: 1,
739
- id: Not(hubId),
740
- },
741
- { status: 0 },
742
- );
743
- }
744
-
745
- // Update the requested config
746
- await this.configRepository.update(hubId, { status });
747
- }
748
-
749
- async deleteConfiguration(configId: number): Promise<void> {
750
- // Find the hub to get the id
751
- const config = await this.configRepository.findOne({
752
- where: { id: configId },
753
- });
754
-
755
- if (!config) {
756
- throw new Error('Integration configuration not found');
757
- }
758
-
759
- await this.configRepository.delete(configId);
760
-
761
- this.logger.log(`Integration configuration deleted: ${configId}`);
762
- }
763
-
764
- async updateConfiguration(
765
- hubId: number,
766
- updateData: {
767
- config?: any;
768
- priority?: number;
769
- is_default?: boolean;
770
- status?: number;
771
- },
772
- ): Promise<IntegrationConfig & { config?: any }> {
773
- // Find the existing hub
774
- const config = await this.configRepository.findOne({
775
- where: { id: hubId },
776
- });
777
-
778
- if (!config) {
779
- throw new Error('Integration configuration not found');
780
- }
781
-
782
- // Update configuration JSON if provided
783
- if (updateData.config) {
784
- // Merge the new config with existing config
785
- const updatedConfigJson = {
786
- ...config.config_json,
787
- ...updateData.config,
788
- };
789
-
790
- await this.configRepository.update(config.id, {
791
- config_json: updatedConfigJson,
792
- } as any);
793
- }
794
-
795
- // Handle default configuration logic (simplified for now)
796
- if (updateData.is_default === true) {
797
- // Remove default from other configurations of same type and level
798
- await this.configRepository.update(
799
- {
800
- level_id: config.level_id,
801
- level_type: config.level_type,
802
- integration_type: config.integration_type,
803
- id: Not(hubId),
804
- },
805
- { is_default: false },
806
- );
807
- }
808
-
809
- // Apply direct config updates if any
810
- const directUpdates: any = {};
811
- if (updateData.priority !== undefined)
812
- directUpdates.priority = updateData.priority;
813
- if (updateData.is_default !== undefined)
814
- directUpdates.is_default = updateData.is_default;
815
- if (updateData.status !== undefined)
816
- directUpdates.status = updateData.status;
817
-
818
- if (Object.keys(directUpdates).length > 0) {
819
- await this.configRepository.update(config.id, directUpdates);
820
- }
821
-
822
- // Fetch and return updated config
823
- const updatedConfig = await this.configRepository.findOne({
824
- where: { id: hubId },
825
- });
826
-
827
- return {
828
- ...updatedConfig,
829
- config: updatedConfig?.config_json,
830
- } as any;
831
- }
832
-
833
- async sendGenericMessage(
834
- messageDto: GenericMessageDto,
835
- ): Promise<IntegrationResult> {
836
- try {
837
- const {
838
- levelId,
839
- levelType,
840
- app_code,
841
- to,
842
- message,
843
- type,
844
- priority = 'medium',
845
- html,
846
- cc,
847
- bcc,
848
- attachments,
849
- mediaUrl,
850
- templateId,
851
- user_id,
852
- entity_id,
853
- entity_type,
854
- } = messageDto;
855
-
856
- let subject = messageDto.subject;
857
-
858
- // Auto-detect communication type if not specified
859
- const communicationType = this.detectCommunicationType(to, type);
860
-
861
- // Get active configs for the detected type
862
- const configs = await this.getActiveConfigs(
863
- levelId,
864
- levelType,
865
- app_code,
866
- communicationType,
867
- );
868
-
869
- if (!configs.length) {
870
- this.logger.warn(
871
- `No communication hubs found for ${levelType} ${levelId}. Please configure integration providers using the createIntegrationConfig method.`,
872
- );
873
- throw new Error(
874
- `No active ${communicationType} configuration found for ${levelType} ${levelId}. Please configure a communication provider first.`,
875
- );
876
- }
877
-
878
- // Sort hubs by priority and default preference
879
- const sortedConfigs = this.sortConfigsByPriorityAndDefault(
880
- configs,
881
- priority,
882
- );
883
-
884
- // Prepare enhanced config with additional parameters
885
- const enhancedConfig = {
886
- subject,
887
- html,
888
- cc,
889
- bcc,
890
- attachments,
891
- mediaUrl,
892
- };
893
-
894
- // Handle multiple recipients by sending to each individually
895
- if (Array.isArray(to)) {
896
- const results: IntegrationResult[] = [];
897
- let hasSuccess = false;
898
-
899
- for (const recipient of to) {
900
- for (const hub of sortedConfigs) {
901
- try {
902
- const serviceType = this.deriveServiceType(
903
- hub.integration_type,
904
- hub.integration_provider,
905
- hub.config_json,
906
- );
907
- const strategy = this.integrationFactory.create(
908
- hub.integration_type,
909
- serviceType,
910
- hub.integration_provider,
911
- );
912
-
913
- // Process template if provided
914
- let variables;
915
- let richText;
916
- let externalTemplateId: string | undefined = undefined;
917
- if (templateId) {
918
- const templateData = await this.processTemplate(
919
- parseInt(templateId, 10),
920
- entity_type,
921
- entity_id,
922
- );
923
- variables = templateData.variables;
924
- externalTemplateId = templateData.externalTemplateId;
925
- richText = templateData.rich_text;
926
- // subject = templateData.subject;
927
- }
928
-
929
- // Merge config with enhanced parameters
930
- let finalConfig = {
931
- ...hub.config_json,
932
- ...enhancedConfig,
933
- templateId: externalTemplateId,
934
- variables,
935
- richText,
936
- };
937
-
938
- // Handle user integration if user_id provided
939
- if (user_id) {
940
- const userIntegrationData =
941
- await this.getUserIntegrationForStrategy(user_id, hub.id);
942
-
943
- if (userIntegrationData) {
944
- finalConfig = {
945
- ...finalConfig,
946
- external_user_id: userIntegrationData.external_user_id,
947
- };
948
- }
949
- }
950
-
951
- const result = await strategy.sendMessage(
952
- recipient,
953
- message,
954
- finalConfig,
955
- );
956
-
957
- if (result.success) {
958
- this.logger.log(
959
- `Generic message sent successfully via ${hub.integration_provider} to ${recipient}`,
960
- );
961
- results.push(result);
962
- hasSuccess = true;
963
- break; // Move to next recipient
964
- }
965
-
966
- this.logger.warn(
967
- `Failed to send via ${hub.integration_provider} to ${recipient}: ${result.error}`,
968
- );
969
- } catch (error) {
970
- this.logger.error(
971
- `Error sending via ${hub.integration_provider} to ${recipient}:`,
972
- error.message,
973
- );
974
- continue;
975
- }
976
- }
977
- }
978
-
979
- if (hasSuccess) {
980
- return {
981
- success: true,
982
- messageId: results.map((r) => r.messageId).join(','),
983
- provider: 'multiple',
984
- service: 'multiple',
985
- timestamp: new Date(),
986
- };
987
- }
988
- } else {
989
- // Handle single recipient
990
- for (const hub of sortedConfigs) {
991
- try {
992
- const serviceType = this.deriveServiceType(
993
- hub.integration_type,
994
- hub.integration_provider,
995
- hub.config_json,
996
- );
997
- const strategy = this.integrationFactory.create(
998
- hub.integration_type,
999
- serviceType,
1000
- hub.integration_provider,
1001
- );
1002
-
1003
- // Process template if provided
1004
- let variables;
1005
- let externalTemplateId: string | undefined = undefined;
1006
- let richText;
1007
- if (templateId) {
1008
- const templateData = await this.processTemplate(
1009
- parseInt(templateId, 10),
1010
- entity_type,
1011
- entity_id,
1012
- );
1013
- variables = templateData.variables;
1014
- externalTemplateId = templateData.externalTemplateId;
1015
- richText = templateData.rich_text;
1016
- // subject = templateData.subject;
1017
- }
1018
-
1019
- // Merge config with enhanced parameters
1020
- let finalConfig = {
1021
- ...hub.config_json,
1022
- ...enhancedConfig,
1023
- templateId: externalTemplateId,
1024
- variables,
1025
- richText,
1026
- };
1027
-
1028
- // Handle user integration if user_id provided
1029
- if (user_id) {
1030
- const userIntegrationData =
1031
- await this.getUserIntegrationForStrategy(user_id, hub.id);
1032
-
1033
- if (userIntegrationData) {
1034
- finalConfig = {
1035
- ...finalConfig,
1036
- external_user_id: userIntegrationData.external_user_id,
1037
- };
1038
- }
1039
- }
1040
-
1041
- const result = await strategy.sendMessage(to, message, finalConfig);
1042
-
1043
- if (result.success) {
1044
- this.logger.log(
1045
- `Generic message sent successfully via ${hub.integration_provider} to ${to}`,
1046
- );
1047
- return result;
1048
- }
1049
-
1050
- this.logger.warn(
1051
- `Failed to send via ${hub.integration_provider}: ${result.error}`,
1052
- );
1053
- } catch (error) {
1054
- this.logger.error(
1055
- `Error sending via ${hub.integration_provider}:`,
1056
- error.message,
1057
- );
1058
- continue;
1059
- }
1060
- }
1061
- }
1062
-
1063
- throw new Error('All communication providers failed');
1064
- } catch (error) {
1065
- this.logger.error('Generic communication service error:', error.message);
1066
- throw error;
1067
- }
1068
- }
1069
-
1070
- async sendBulkMessage(
1071
- bulkDto: BulkMessageDto,
1072
- ): Promise<{ results: IntegrationResult[]; summary: any }> {
1073
- try {
1074
- const {
1075
- levelId,
1076
- levelType,
1077
- app_code,
1078
- recipients,
1079
- message,
1080
- type,
1081
- priority = 'low',
1082
- subject,
1083
- html,
1084
- templateId,
1085
- variables,
1086
- batchSize = 10,
1087
- } = bulkDto;
1088
-
1089
- const results: IntegrationResult[] = [];
1090
- const batches = this.chunkArray(recipients, batchSize);
1091
-
1092
- for (let i = 0; i < batches.length; i++) {
1093
- const batch = batches[i];
1094
- this.logger.log(
1095
- `Processing batch ${i + 1}/${batches.length} with ${batch.length} recipients`,
1096
- );
1097
-
1098
- const batchPromises = batch.map(async (recipient) => {
1099
- try {
1100
- const messageDto: GenericMessageDto = {
1101
- levelId,
1102
- levelType,
1103
- app_code,
1104
- to: recipient,
1105
- message,
1106
- type,
1107
- priority,
1108
- subject,
1109
- html,
1110
- templateId,
1111
- variables,
1112
- };
1113
-
1114
- return await this.sendGenericMessage(messageDto);
1115
- } catch (error) {
1116
- return {
1117
- success: false,
1118
- provider: 'unknown',
1119
- service: 'unknown',
1120
- error: error.message,
1121
- timestamp: new Date(),
1122
- } as IntegrationResult;
1123
- }
1124
- });
1125
-
1126
- const batchResults = await Promise.allSettled(batchPromises);
1127
- const processedResults = batchResults.map((result) => {
1128
- if (result.status === 'fulfilled') {
1129
- return result.value;
1130
- } else {
1131
- return {
1132
- success: false,
1133
- provider: 'unknown',
1134
- service: 'unknown',
1135
- error: result.reason?.message || 'Unknown error',
1136
- timestamp: new Date(),
1137
- } as IntegrationResult;
1138
- }
1139
- });
1140
-
1141
- results.push(...processedResults);
1142
-
1143
- // Rate limiting delay between batches
1144
- if (i < batches.length - 1) {
1145
- await new Promise((resolve) => setTimeout(resolve, 1000));
1146
- }
1147
- }
1148
-
1149
- const summary = {
1150
- total: results.length,
1151
- successful: results.filter((r) => r.success).length,
1152
- failed: results.filter((r) => !r.success).length,
1153
- successRate:
1154
- (
1155
- (results.filter((r) => r.success).length / results.length) *
1156
- 100
1157
- ).toFixed(2) + '%',
1158
- };
1159
-
1160
- this.logger.log(
1161
- `Bulk message completed: ${summary.successful}/${summary.total} successful`,
1162
- );
1163
-
1164
- return { results, summary };
1165
- } catch (error) {
1166
- this.logger.error('Bulk communication service error:', error.message);
1167
- throw error;
1168
- }
1169
- }
1170
-
1171
- async scheduleMessage(
1172
- scheduledDto: any,
1173
- ): Promise<{ scheduled: boolean; scheduleId?: string }> {
1174
- // For now, return a placeholder. In production, integrate with a job queue like Bull or Agenda
1175
- this.logger.log(`Message scheduled for ${scheduledDto.scheduleFor}`);
1176
-
1177
- return {
1178
- scheduled: true,
1179
- scheduleId: `sched_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
1180
- };
1181
- }
1182
-
1183
- async sendTemplateMessage(templateDto: any): Promise<IntegrationResult> {
1184
- // Get template content (integrate with your template system)
1185
- const template = await this.getTemplate(templateDto.templateId);
1186
-
1187
- const processedMessage = this.replaceTemplateVariables(
1188
- template.content,
1189
- templateDto.variables,
1190
- );
1191
- const processedSubject = template.subject
1192
- ? this.replaceTemplateVariables(template.subject, templateDto.variables)
1193
- : undefined;
1194
-
1195
- const messageDto: GenericMessageDto = {
1196
- levelId: templateDto.levelId,
1197
- levelType: templateDto.levelType,
1198
- app_code: templateDto.app_code,
1199
- to: templateDto.to,
1200
- message: processedMessage,
1201
- subject: processedSubject,
1202
- type: templateDto.type,
1203
- templateId: templateDto.templateId,
1204
- variables: templateDto.variables,
1205
- };
1206
-
1207
- return this.sendGenericMessage(messageDto);
1208
- }
1209
-
1210
- private detectCommunicationType(
1211
- to: string | string[],
1212
- type?: string,
1213
- ): 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE' {
1214
- if (type) {
1215
- return type as 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
1216
- }
1217
-
1218
- const recipient = Array.isArray(to) ? to[0] : to;
1219
-
1220
- // Email detection
1221
- if (recipient.includes('@')) {
1222
- return 'EMAIL';
1223
- }
1224
-
1225
- // Phone number detection (basic)
1226
- if (/^\+?[\d\s\-\(\)]+$/.test(recipient)) {
1227
- // Could be SMS or WhatsApp, default to SMS
1228
- return 'SMS';
1229
- }
1230
-
1231
- // Default to EMAIL if unsure
1232
- return 'EMAIL';
1233
- }
1234
-
1235
- private sortConfigsByPriorityAndDefault(
1236
- configs: IntegrationConfigWithConfig[],
1237
- _priority: string,
1238
- ): IntegrationConfigWithConfig[] {
1239
- return configs.sort((a, b) => {
1240
- // First sort by default (true comes first)
1241
- if (a.is_default !== b.is_default) {
1242
- return b.is_default ? 1 : -1;
1243
- }
1244
-
1245
- // Then sort by priority (lower number = higher priority)
1246
- if (a.priority !== b.priority) {
1247
- return a.priority - b.priority;
1248
- }
1249
-
1250
- // Finally sort by created_at descending (newest first)
1251
- return (
1252
- new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
1253
- );
1254
- });
1255
- }
1256
-
1257
- private async processTemplate(
1258
- templateId: number,
1259
- entity_type: any,
1260
- entity_id: any,
1261
- ): Promise<{
1262
- variables?: Record<string, any>;
1263
- externalTemplateId?: string;
1264
- rich_text?: string;
1265
- }> {
1266
- const commTemplate: any = await this.entityService.getEntityData(
1267
- COMM_TEMPLATE,
1268
- templateId,
1269
- {} as any,
1270
- );
1271
- if (!commTemplate) {
1272
- return {
1273
- variables: {},
1274
- externalTemplateId: undefined,
1275
- };
1276
- }
1277
-
1278
- let variables = {};
1279
- if (commTemplate.mapper_id) {
1280
- variables = await this.fieldMapperService.resolveData(
1281
- commTemplate.mapper_id,
1282
- 'LOOKUP',
1283
- entity_type,
1284
- entity_id,
1285
- {} as any,
1286
- );
1287
- }
1288
-
1289
- if (!commTemplate.template_id) {
1290
- let richText = commTemplate.rich_text;
1291
-
1292
- richText = richText.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1293
- const value = variables[key];
1294
- return value !== undefined ? String(value) : match;
1295
- });
1296
-
1297
- return {
1298
- rich_text: richText,
1299
- // subject: commTemplate.subject,
1300
- };
1301
- }
1302
-
1303
- return {
1304
- variables,
1305
- externalTemplateId: commTemplate.template_id,
1306
- };
1307
- }
1308
-
1309
- private replaceTemplateVariables(
1310
- template: string,
1311
- variables: Record<string, any>,
1312
- ): string {
1313
- return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1314
- const value = variables[key];
1315
- return value !== undefined ? String(value) : match;
1316
- });
1317
- }
1318
-
1319
- private async getTemplate(templateId: string) {
1320
- // Placeholder for template system integration
1321
- // In production, this would connect to your template database/service
1322
- return {
1323
- id: templateId,
1324
- subject: 'Default Subject {{variable}}',
1325
- content: 'Hello {{name}}, this is a template message with {{variable}}.',
1326
- };
1327
- }
1328
-
1329
- private chunkArray<T>(array: T[], chunkSize: number): T[][] {
1330
- const chunks: T[][] = [];
1331
- for (let i = 0; i < array.length; i += chunkSize) {
1332
- chunks.push(array.slice(i, i + chunkSize));
1333
- }
1334
- return chunks;
1335
- }
1336
-
1337
- async initGmailOAuth(
1338
- levelId: number,
1339
- levelType: string,
1340
- app_code: string,
1341
- email?: string,
1342
- ): Promise<{ authUrl: string; state: string }> {
1343
- try {
1344
- // Get system OAuth credentials from config
1345
- const clientId = this.configService.get<string>('CLIENT_ID');
1346
- const clientSecret = this.configService.get<string>('CLIENT_SECRET');
1347
-
1348
- if (!clientId || !clientSecret) {
1349
- throw new Error('Gmail OAuth credentials not configured');
1350
- }
1351
-
1352
- const state = this.generateSecureState();
1353
- // Use the existing registered callback URL
1354
- const callbackUrl =
1355
- this.configService.get<string>('CALLBACK_URL') ||
1356
- 'http://localhost:5001/auth/google/callback';
1357
-
1358
- this.gmailOAuthStates.set(state, {
1359
- levelId,
1360
- levelType,
1361
- app_code,
1362
- email,
1363
- timestamp: Date.now(),
1364
- });
1365
-
1366
- // Auto-cleanup after 10 minutes
1367
- setTimeout(
1368
- () => {
1369
- this.gmailOAuthStates.delete(state);
1370
- },
1371
- 10 * 60 * 1000,
1372
- );
1373
-
1374
- const oauth2Client = new google.auth.OAuth2(
1375
- clientId,
1376
- clientSecret,
1377
- callbackUrl,
1378
- );
1379
-
1380
- const scopes = [
1381
- 'https://www.googleapis.com/auth/gmail.send',
1382
- 'https://www.googleapis.com/auth/gmail.readonly',
1383
- 'https://www.googleapis.com/auth/userinfo.email',
1384
- 'https://www.googleapis.com/auth/calendar',
1385
- ];
1386
-
1387
- const authUrl = oauth2Client.generateAuthUrl({
1388
- access_type: 'offline',
1389
- scope: scopes,
1390
- state: `gmail_config:${state}`, // Prefix to identify Gmail config requests
1391
- prompt: 'consent',
1392
- login_hint: email,
1393
- });
1394
-
1395
- return { authUrl, state };
1396
- } catch (error) {
1397
- this.logger.error('Error initializing Gmail OAuth:', error.message);
1398
- throw new Error('Failed to initialize Gmail OAuth');
1399
- }
1400
- }
1401
-
1402
- async handleGmailOAuthCallback(
1403
- code: string,
1404
- state: string,
1405
- ): Promise<GmailSSOResult> {
1406
- try {
1407
- const oauthState = this.gmailOAuthStates.get(state);
1408
- if (!oauthState) {
1409
- throw new Error('Invalid or expired OAuth state');
1410
- }
1411
-
1412
- this.gmailOAuthStates.delete(state);
1413
-
1414
- if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1415
- throw new Error('OAuth state expired');
1416
- }
1417
-
1418
- // Get system OAuth credentials
1419
- const clientId = this.configService.get<string>('CLIENT_ID');
1420
- const clientSecret = this.configService.get<string>('CLIENT_SECRET');
1421
- const callbackUrl =
1422
- this.configService.get<string>('CALLBACK_URL') ||
1423
- 'http://localhost:5001/auth/google/callback';
1424
-
1425
- const oauth2Client = new google.auth.OAuth2(
1426
- clientId,
1427
- clientSecret,
1428
- callbackUrl,
1429
- );
1430
-
1431
- const { tokens } = await oauth2Client.getToken(code);
1432
-
1433
- if (!tokens.access_token) {
1434
- throw new Error('Failed to obtain access token');
1435
- }
1436
-
1437
- // Get user email from Google
1438
- oauth2Client.setCredentials(tokens);
1439
- const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
1440
- const userInfo = await oauth2.userinfo.get();
1441
- const email = userInfo.data.email;
1442
-
1443
- if (!email) {
1444
- throw new Error('Failed to get user email');
1445
- }
1446
-
1447
- // Verify email matches if hint was provided
1448
- if (oauthState.email && oauthState.email !== email) {
1449
- throw new Error('Email mismatch with OAuth hint');
1450
- }
1451
-
1452
- const gmailConfig = {
1453
- clientId,
1454
- clientSecret,
1455
- email: email,
1456
- accessToken: tokens.access_token,
1457
- refreshToken: tokens.refresh_token,
1458
- scope: tokens.scope,
1459
- tokenType: tokens.token_type,
1460
- expiryDate: tokens.expiry_date,
1461
- };
1462
-
1463
- // Validate that no active EMAIL configuration exists
1464
- await this.validateUniqueActiveConfig(
1465
- oauthState.levelId,
1466
- oauthState.levelType,
1467
- oauthState.app_code,
1468
- 'EMAIL',
1469
- 'gmail',
1470
- );
1471
-
1472
- // Create integration config
1473
- const config = this.configRepository.create({
1474
- app_code: oauthState.app_code,
1475
- integration_type: 'EMAIL',
1476
- integration_provider: 'gmail',
1477
- integration_source_id: 1, // Gmail source ID
1478
- level_id: oauthState.levelId,
1479
- level_type: oauthState.levelType,
1480
- status: 1,
1481
- priority: 1,
1482
- is_default: false,
1483
- config_json: gmailConfig as any,
1484
- });
1485
-
1486
- const savedConfig = await this.configRepository.save(config);
1487
-
1488
- this.logger.log(
1489
- `Gmail OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1490
- );
1491
-
1492
- return {
1493
- hubId: savedConfig.id,
1494
- configId: savedConfig.id,
1495
- };
1496
- } catch (error) {
1497
- this.logger.error('Error handling Gmail OAuth callback:', error.message);
1498
- throw new Error(`Failed to complete Gmail OAuth: ${error.message}`);
1499
- }
1500
- }
1501
-
1502
- async handleGmailTokensCallback(
1503
- email: string,
1504
- accessToken: string,
1505
- refreshToken: string,
1506
- state: string,
1507
- ): Promise<GmailSSOResult> {
1508
- try {
1509
- const oauthState = this.gmailOAuthStates.get(state);
1510
- if (!oauthState) {
1511
- throw new Error('Invalid or expired OAuth state');
1512
- }
1513
-
1514
- this.gmailOAuthStates.delete(state);
1515
-
1516
- if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1517
- throw new Error('OAuth state expired');
1518
- }
1519
-
1520
- // Verify email matches if hint was provided
1521
- if (oauthState.email && oauthState.email !== email) {
1522
- throw new Error('Email mismatch with OAuth hint');
1523
- }
1524
-
1525
- // Validate that no active EMAIL configuration exists
1526
- await this.validateUniqueActiveConfig(
1527
- oauthState.levelId,
1528
- oauthState.levelType,
1529
- oauthState.app_code,
1530
- 'EMAIL',
1531
- 'gmail',
1532
- );
1533
-
1534
- // Pure SSO configuration - no client credentials stored per user
1535
- const gmailConfig = {
1536
- email: email,
1537
- accessToken: accessToken,
1538
- refreshToken: refreshToken,
1539
- authMethod: 'GOOGLE_SSO',
1540
- scopes: [
1541
- 'https://www.googleapis.com/auth/gmail.send',
1542
- 'https://www.googleapis.com/auth/userinfo.email',
1543
- ],
1544
- };
1545
-
1546
- // Create integration config
1547
- const config = this.configRepository.create({
1548
- app_code: oauthState.app_code,
1549
- integration_type: 'EMAIL',
1550
- integration_provider: 'gmail',
1551
- integration_source_id: 1, // Gmail source ID
1552
- level_id: oauthState.levelId,
1553
- level_type: oauthState.levelType,
1554
- status: 1,
1555
- priority: 1,
1556
- is_default: false,
1557
- config_json: gmailConfig as any,
1558
- });
1559
-
1560
- const savedConfig = await this.configRepository.save(config);
1561
-
1562
- this.logger.log(
1563
- `Gmail tokens configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1564
- );
1565
-
1566
- return {
1567
- hubId: savedConfig.id,
1568
- configId: savedConfig.id,
1569
- };
1570
- } catch (error) {
1571
- this.logger.error('Error handling Gmail tokens callback:', error.message);
1572
- throw new Error(
1573
- `Failed to complete Gmail tokens callback: ${error.message}`,
1574
- );
1575
- }
1576
- }
1577
-
1578
- async testGmailConfig(
1579
- hubId: number,
1580
- ): Promise<{ success: boolean; error?: string }> {
1581
- try {
1582
- const integrationConfig = await this.configRepository.findOne({
1583
- where: { id: hubId },
1584
- });
1585
-
1586
- if (!integrationConfig) {
1587
- throw new Error('Integration config not found');
1588
- }
1589
-
1590
- const isValid = await this.gmailApiStrategy.validateConnection(
1591
- integrationConfig.config_json,
1592
- );
1593
-
1594
- if (!isValid) {
1595
- return {
1596
- success: false,
1597
- error: 'Gmail configuration is invalid or expired',
1598
- };
1599
- }
1600
-
1601
- return { success: true };
1602
- } catch (error) {
1603
- this.logger.error('Error testing Gmail config:', error.message);
1604
- return { success: false, error: error.message };
1605
- }
1606
- }
1607
-
1608
- private generateSecureState(): string {
1609
- return Math.random().toString(36).substring(2) + Date.now().toString(36);
1610
- }
1611
-
1612
- private async validateUniqueActiveConfig(
1613
- levelId: number,
1614
- levelType: string,
1615
- app_code: string,
1616
- configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1617
- provider: string,
1618
- excludeHubId?: number,
1619
- ): Promise<void> {
1620
- // Find all active configurations of the same provider for this level and app_code
1621
- const query = this.configRepository
1622
- .createQueryBuilder('hub')
1623
- .where('hub.level_id = :levelId', { levelId })
1624
- .andWhere('hub.level_type = :levelType', { levelType })
1625
- .andWhere('hub.app_code = :app_code', { app_code })
1626
- .andWhere('hub.integration_type = :configType', { configType })
1627
- .andWhere('hub.integration_provider = :provider', { provider })
1628
- .andWhere('hub.status = :status', { status: 1 });
1629
-
1630
- // Exclude current hub if updating
1631
- if (excludeHubId) {
1632
- query.andWhere('hub.id != :excludeHubId', { excludeHubId });
1633
- }
1634
-
1635
- const existingActiveConfigs = await query.getMany();
1636
-
1637
- // If there are existing active configurations of the same provider, throw error
1638
- if (existingActiveConfigs.length > 0) {
1639
- throw new Error(
1640
- `A ${provider} configuration already exists for ${configType} in app_code ${app_code}, ${levelType} ${levelId}. Only one configuration per provider is allowed.`,
1641
- );
1642
- }
1643
- }
1644
-
1645
- private requiresOAuthFlow(
1646
- configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1647
- provider: string,
1648
- config: any,
1649
- ): boolean {
1650
- // Check if config indicates OAuth is needed (missing tokens or explicit OAuth request)
1651
- const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
1652
-
1653
- switch (key) {
1654
- case 'email_gmail':
1655
- // Requires OAuth if no access token provided or OAuth explicitly requested
1656
- return !config.accessToken || config.useOAuth === true;
1657
-
1658
- case 'email_outlook':
1659
- // Requires OAuth if no access token provided or OAuth explicitly requested
1660
- return !config.accessToken || config.useOAuth === true;
1661
-
1662
- default:
1663
- // Most other providers don't require OAuth
1664
- return false;
1665
- }
1666
- }
1667
-
1668
- private async generateOAuthUrl(
1669
- levelId: number,
1670
- levelType: string,
1671
- app_code: string,
1672
- configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1673
- provider: string,
1674
- integration_source_id: number,
1675
- config: any,
1676
- priority?: number,
1677
- is_default?: boolean,
1678
- ): Promise<{ authUrl: string; state: string; message: string }> {
1679
- const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
1680
-
1681
- switch (key) {
1682
- case 'email_gmail':
1683
- const gmailResult = await this.initGmailOAuth(
1684
- levelId,
1685
- levelType,
1686
- app_code,
1687
- config.email,
1688
- );
1689
- return {
1690
- authUrl: gmailResult.authUrl,
1691
- state: gmailResult.state,
1692
- message:
1693
- 'Please complete Gmail OAuth authorization. Configuration will be created automatically after authorization.',
1694
- };
1695
-
1696
- case 'email_outlook':
1697
- const outlookResult = await this.initOutlookOAuth(
1698
- levelId,
1699
- levelType,
1700
- app_code,
1701
- config.email,
1702
- );
1703
- return {
1704
- authUrl: outlookResult.authUrl,
1705
- state: outlookResult.state,
1706
- message:
1707
- 'Please complete Outlook OAuth authorization. Configuration will be created automatically after authorization.',
1708
- };
1709
-
1710
- default:
1711
- throw new Error(`OAuth not supported for ${configType}/${provider}`);
1712
- }
1713
- }
1714
-
1715
- private async createDirectConfig(
1716
- levelId: number,
1717
- levelType: string,
1718
- app_code: string,
1719
- configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1720
- provider: string,
1721
- integration_source_id: number,
1722
- config: any,
1723
- priority?: number,
1724
- is_default?: boolean,
1725
- ): Promise<IntegrationConfig> {
1726
- // Validate that no duplicate provider configurations exist
1727
- await this.validateUniqueActiveConfig(
1728
- levelId,
1729
- levelType,
1730
- app_code,
1731
- configType,
1732
- provider,
1733
- );
1734
-
1735
- // Deactivate all other configurations of the same integration type
1736
- await this.configRepository.update(
1737
- {
1738
- level_id: levelId,
1739
- level_type: levelType,
1740
- app_code: app_code,
1741
- integration_type: configType,
1742
- status: 1,
1743
- },
1744
- { status: 0 },
1745
- );
1746
-
1747
- // If setting as default, remove default from other configurations of same type and app_code
1748
- if (is_default) {
1749
- await this.configRepository.update(
1750
- {
1751
- level_id: levelId,
1752
- level_type: levelType,
1753
- app_code: app_code,
1754
- integration_type: configType,
1755
- },
1756
- { is_default: false },
1757
- );
1758
- }
1759
-
1760
- // Create integration config
1761
- const integrationConfig = this.configRepository.create({
1762
- app_code: app_code,
1763
- integration_type: configType,
1764
- integration_provider: provider,
1765
- integration_source_id: integration_source_id,
1766
- level_id: levelId,
1767
- level_type: levelType,
1768
- status: 1,
1769
- priority: priority || 1,
1770
- is_default: is_default || false,
1771
- config_json: config,
1772
- });
1773
-
1774
- const savedConfig = await this.configRepository.save(integrationConfig);
1775
- this.logger.log(
1776
- `Communication config created: ${configType}/${provider} for ${levelType} ${levelId}`,
1777
- );
1778
-
1779
- // Store DID mapping for TELEPHONE integration
1780
- if (configType === 'TELEPHONE') {
1781
- try {
1782
- // Extract DID from config (could be did, callerNumber, or fromNumber)
1783
- const did = config.did || config.callerNumber || config.fromNumber;
1784
-
1785
- if (did) {
1786
- const entityMapper = this.entityMapperRepository.create({
1787
- integration_config_id: savedConfig.id,
1788
- level_id: String(levelId),
1789
- level_type: levelType,
1790
- appcode: app_code,
1791
- did: did,
1792
- campaign_name: config.campaignName || null,
1793
- });
1794
-
1795
- await this.entityMapperRepository.save(entityMapper);
1796
- this.logger.log(
1797
- `DID mapping created for TELEPHONE integration: ${did} for ${levelType} ${levelId}`,
1798
- );
1799
- }
1800
- } catch (error) {
1801
- this.logger.warn(
1802
- `Failed to create DID mapping for TELEPHONE integration: ${error.message}`,
1803
- );
1804
- }
1805
- }
1806
-
1807
- return Array.isArray(savedConfig) ? savedConfig[0] : savedConfig;
1808
- }
1809
-
1810
- async initOutlookOAuth(
1811
- levelId: number,
1812
- levelType: string,
1813
- app_code: string,
1814
- email?: string,
1815
- ): Promise<{ authUrl: string; state: string }> {
1816
- try {
1817
- // Get system OAuth credentials from config
1818
- const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
1819
- const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
1820
-
1821
- if (!clientId || !tenantId) {
1822
- throw new Error('Outlook OAuth credentials not configured');
1823
- }
1824
-
1825
- const state = this.generateSecureState();
1826
- const callbackUrl =
1827
- this.configService.get<string>('OUTLOOK_CALLBACK_URL') ||
1828
- 'http://localhost:5001/auth/outlook/callback';
1829
-
1830
- this.gmailOAuthStates.set(state, {
1831
- levelId,
1832
- levelType,
1833
- app_code,
1834
- email,
1835
- timestamp: Date.now(),
1836
- });
1837
-
1838
- // Auto-cleanup after 10 minutes
1839
- setTimeout(
1840
- () => {
1841
- this.gmailOAuthStates.delete(state);
1842
- },
1843
- 10 * 60 * 1000,
1844
- );
1845
-
1846
- const scopes = [
1847
- 'https://graph.microsoft.com/Mail.Send',
1848
- 'https://graph.microsoft.com/User.Read',
1849
- ];
1850
-
1851
- const authUrl =
1852
- `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` +
1853
- `client_id=${clientId}&` +
1854
- `response_type=code&` +
1855
- `redirect_uri=${encodeURIComponent(callbackUrl)}&` +
1856
- `scope=${encodeURIComponent(scopes.join(' '))}&` +
1857
- `state=outlook_config:${state}&` +
1858
- `response_mode=query&` +
1859
- `prompt=consent` +
1860
- (email ? `&login_hint=${encodeURIComponent(email)}` : '');
1861
-
1862
- return { authUrl, state };
1863
- } catch (error) {
1864
- this.logger.error('Error initializing Outlook OAuth:', error.message);
1865
- throw new Error('Failed to initialize Outlook OAuth');
1866
- }
1867
- }
1868
-
1869
- async handleOutlookOAuthCallback(
1870
- code: string,
1871
- state: string,
1872
- ): Promise<GmailSSOResult> {
1873
- try {
1874
- const oauthState = this.gmailOAuthStates.get(state);
1875
- if (!oauthState) {
1876
- throw new Error('Invalid or expired OAuth state');
1877
- }
1878
-
1879
- this.gmailOAuthStates.delete(state);
1880
-
1881
- if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1882
- throw new Error('OAuth state expired');
1883
- }
1884
-
1885
- const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
1886
- const clientSecret = this.configService.get<string>(
1887
- 'OUTLOOK_CLIENT_SECRET',
1888
- );
1889
- const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
1890
- const callbackUrl =
1891
- this.configService.get<string>('OUTLOOK_CALLBACK_URL') ||
1892
- 'http://localhost:5001/auth/outlook/callback';
1893
-
1894
- // Exchange code for tokens
1895
- const tokenResponse = await fetch(
1896
- `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
1897
- {
1898
- method: 'POST',
1899
- headers: {
1900
- 'Content-Type': 'application/x-www-form-urlencoded',
1901
- },
1902
- body: new URLSearchParams({
1903
- client_id: clientId!,
1904
- client_secret: clientSecret!,
1905
- code: code,
1906
- redirect_uri: callbackUrl,
1907
- grant_type: 'authorization_code',
1908
- }),
1909
- },
1910
- );
1911
-
1912
- const tokens = await tokenResponse.json();
1913
-
1914
- if (!tokens.access_token) {
1915
- throw new Error('Failed to obtain access token');
1916
- }
1917
-
1918
- // Get user info from Microsoft Graph
1919
- const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
1920
- headers: {
1921
- Authorization: `Bearer ${tokens.access_token}`,
1922
- },
1923
- });
1924
-
1925
- const userInfo = await userResponse.json();
1926
- const email = userInfo.mail || userInfo.userPrincipalName;
1927
-
1928
- if (!email) {
1929
- throw new Error('Failed to get user email');
1930
- }
1931
-
1932
- // Validate that no active EMAIL configuration exists
1933
- await this.validateUniqueActiveConfig(
1934
- oauthState.levelId,
1935
- oauthState.levelType,
1936
- oauthState.app_code,
1937
- 'EMAIL',
1938
- 'outlook',
1939
- );
1940
-
1941
- const outlookConfig = {
1942
- clientId,
1943
- tenantId,
1944
- email: email,
1945
- accessToken: tokens.access_token,
1946
- refreshToken: tokens.refresh_token,
1947
- scope: tokens.scope,
1948
- tokenType: tokens.token_type,
1949
- expiresIn: tokens.expires_in,
1950
- };
1951
-
1952
- // Create integration config
1953
- const config = this.configRepository.create({
1954
- app_code: oauthState.app_code,
1955
- integration_type: 'EMAIL',
1956
- integration_provider: 'outlook',
1957
- integration_source_id: 1, // Outlook source ID
1958
- level_id: oauthState.levelId,
1959
- level_type: oauthState.levelType,
1960
- status: 1,
1961
- priority: 1,
1962
- is_default: false,
1963
- config_json: outlookConfig as any,
1964
- });
1965
-
1966
- const savedConfig = await this.configRepository.save(config);
1967
-
1968
- this.logger.log(
1969
- `Outlook OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1970
- );
1971
-
1972
- return {
1973
- hubId: savedConfig.id,
1974
- configId: savedConfig.id,
1975
- };
1976
- } catch (error) {
1977
- this.logger.error(
1978
- 'Error handling Outlook OAuth callback:',
1979
- error.message,
1980
- );
1981
- throw new Error(`Failed to complete Outlook OAuth: ${error.message}`);
1982
- }
1983
- }
1984
-
1985
- async getIntegrationConfigById(hubId: number): Promise<
1986
- | (IntegrationConfig & {
1987
- config: IntegrationConfig;
1988
- linkedSource?: string;
1989
- configDetails?: any;
1990
- })
1991
- | null
1992
- > {
1993
- try {
1994
- // Find the integration config by ID
1995
- const integrationConfig = await this.configRepository.findOne({
1996
- where: { id: hubId },
1997
- });
1998
-
1999
- if (!integrationConfig) {
2000
- return null;
2001
- }
2002
-
2003
- // Extract linked source and config details
2004
- const linkedSource = this.extractLinkedSource(
2005
- integrationConfig.integration_type,
2006
- integrationConfig.integration_provider,
2007
- integrationConfig.config_json,
2008
- );
2009
-
2010
- const configDetails = this.extractConfigDetails(
2011
- integrationConfig.integration_type,
2012
- integrationConfig.integration_provider,
2013
- integrationConfig.config_json,
2014
- );
2015
-
2016
- return {
2017
- ...integrationConfig,
2018
- config: integrationConfig,
2019
- linkedSource,
2020
- configDetails,
2021
- };
2022
- } catch (error) {
2023
- this.logger.error(
2024
- `Error fetching communication config by ID ${hubId}:`,
2025
- error.message,
2026
- );
2027
- throw new Error(
2028
- `Failed to fetch communication configuration: ${error.message}`,
2029
- );
2030
- }
2031
- }
2032
-
2033
- async getIntegrationTemplates(
2034
- levelId: number,
2035
- levelType: string,
2036
- app_code: string,
2037
- integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
2038
- ): Promise<{
2039
- success: boolean;
2040
- data?: Array<{
2041
- label: string;
2042
- value: string;
2043
- }>;
2044
- error?: string;
2045
- }> {
2046
- try {
2047
- // Get active configuration for the integration type
2048
- const config = await this.getSingleActiveConfig(
2049
- levelId,
2050
- levelType,
2051
- app_code,
2052
- integration_type,
2053
- );
2054
-
2055
- if (!config) {
2056
- return {
2057
- success: false,
2058
- error: `No active ${integration_type} configuration found for this level`,
2059
- };
2060
- }
2061
-
2062
- // Create strategy instance
2063
- const strategy = this.integrationFactory.create(
2064
- config.integration_type,
2065
- 'API',
2066
- config.integration_provider,
2067
- );
2068
-
2069
- // Check if strategy supports getTemplates
2070
- if (typeof (strategy as any).getTemplates !== 'function') {
2071
- return {
2072
- success: false,
2073
- error: `Template retrieval not supported for provider: ${config.integration_provider}`,
2074
- };
2075
- }
2076
-
2077
- // Call getTemplates method
2078
- return await (strategy as any).getTemplates(config.config_json);
2079
- } catch (error) {
2080
- this.logger.error(
2081
- `Error fetching templates for ${integration_type} level ${levelId}/${levelType}:`,
2082
- error.message,
2083
- );
2084
- return {
2085
- success: false,
2086
- error: `Failed to fetch templates: ${error.message}`,
2087
- };
2088
- }
2089
- }
2090
-
2091
- async getSendGridTemplates(
2092
- levelId: number,
2093
- levelType: string,
2094
- app_code: string,
2095
- ): Promise<{
2096
- success: boolean;
2097
- data?: Array<{
2098
- label: string;
2099
- value: string;
2100
- }>;
2101
- error?: string;
2102
- }> {
2103
- try {
2104
- // Find active SendGrid configurations for this level
2105
- const hubs = await this.getActiveConfigs(
2106
- levelId,
2107
- levelType,
2108
- app_code,
2109
- 'EMAIL',
2110
- );
2111
-
2112
- // Look for SendGrid provider
2113
- const sendGridHub = hubs.find(
2114
- (hub) => hub.integration_provider === 'sendgrid',
2115
- );
2116
-
2117
- if (!sendGridHub) {
2118
- return {
2119
- success: false,
2120
- error: 'No active SendGrid configuration found for this level',
2121
- };
2122
- }
2123
-
2124
- // Extract API key from configuration
2125
- const apiKey = sendGridHub.config_json?.apiKey;
2126
-
2127
- if (!apiKey) {
2128
- return {
2129
- success: false,
2130
- error: 'SendGrid API key not found in configuration',
2131
- };
2132
- }
2133
-
2134
- // Use the SendGrid strategy to fetch templates
2135
- return this.sendGridApiStrategy.getTemplates(apiKey);
2136
- } catch (error) {
2137
- this.logger.error(
2138
- `Error fetching SendGrid templates for level ${levelId}/${levelType}:`,
2139
- error.message,
2140
- );
2141
- return {
2142
- success: false,
2143
- error: `Failed to fetch SendGrid templates: ${error.message}`,
2144
- };
2145
- }
2146
- }
2147
-
2148
- async getSendGridVerifiedSenders(apiKey: string): Promise<{
2149
- success: boolean;
2150
- data?: Array<{
2151
- label: string;
2152
- value: string;
2153
- }>;
2154
- error?: string;
2155
- }> {
2156
- try {
2157
- if (!apiKey) {
2158
- return {
2159
- success: false,
2160
- error: 'SendGrid API key not found in configuration',
2161
- };
2162
- }
2163
-
2164
- // Use the SendGrid strategy to fetch verified senders
2165
- return this.sendGridApiStrategy.getVerifiedSenders(apiKey);
2166
- } catch (error) {
2167
- this.logger.error(
2168
- `Error fetching SendGrid verified senders`,
2169
- error.message,
2170
- );
2171
- return {
2172
- success: false,
2173
- error: `Failed to fetch SendGrid verified senders: ${error.message}`,
2174
- };
2175
- }
2176
- }
2177
-
2178
- // UserIntegration Management Methods
2179
-
2180
- async createUserIntegration(
2181
- createDto: CreateUserIntegrationDto,
2182
- ): Promise<UserIntegration> {
2183
- try {
2184
- // Check if mapping already exists
2185
- const existing = await this.userIntegrationRepository.findOne({
2186
- where: {
2187
- user_id: createDto.user_id,
2188
- integration_config_id: createDto.integration_config_id,
2189
- },
2190
- });
2191
-
2192
- if (existing) {
2193
- throw new Error('User integration mapping already exists');
2194
- }
2195
-
2196
- // Verify integration config exists
2197
- const config = await this.configRepository.findOne({
2198
- where: { id: createDto.integration_config_id },
2199
- });
2200
-
2201
- if (!config) {
2202
- throw new Error('Integration configuration not found');
2203
- }
2204
-
2205
- const userIntegration = this.userIntegrationRepository.create(createDto);
2206
- return await this.userIntegrationRepository.save(userIntegration);
2207
- } catch (error) {
2208
- this.logger.error(
2209
- `Error creating user integration mapping: ${error.message}`,
2210
- );
2211
- throw error;
2212
- }
2213
- }
2214
-
2215
- async bulkCreateUserIntegration(
2216
- bulkDto: BulkCreateUserIntegrationDto,
2217
- ): Promise<{
2218
- created: UserIntegration[];
2219
- updated: UserIntegration[];
2220
- failed: Array<{ data: CreateUserIntegrationDto; error: string }>;
2221
- summary: {
2222
- total: number;
2223
- created: number;
2224
- updated: number;
2225
- failed: number;
2226
- };
2227
- }> {
2228
- const created: UserIntegration[] = [];
2229
- const updated: UserIntegration[] = [];
2230
- const failed: Array<{ data: CreateUserIntegrationDto; error: string }> = [];
2231
-
2232
- try {
2233
- for (const createDto of bulkDto.user_integrations) {
2234
- try {
2235
- // Check if mapping already exists
2236
- const existing = await this.userIntegrationRepository.findOne({
2237
- where: {
2238
- user_id: createDto.user_id,
2239
- integration_config_id: createDto.integration_config_id,
2240
- },
2241
- });
2242
-
2243
- if (existing) {
2244
- // Update existing mapping
2245
- if (createDto.external_user_id !== undefined) {
2246
- existing.external_user_id = createDto.external_user_id;
2247
- }
2248
- if (createDto.external_user_data !== undefined) {
2249
- existing.external_user_data = createDto.external_user_data;
2250
- }
2251
- if (createDto.is_active !== undefined) {
2252
- existing.is_active = createDto.is_active;
2253
- }
2254
-
2255
- const updatedIntegration =
2256
- await this.userIntegrationRepository.save(existing);
2257
- updated.push(updatedIntegration);
2258
-
2259
- this.logger.log(
2260
- `Updated user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`,
2261
- );
2262
- } else {
2263
- // Verify integration config exists
2264
- const config = await this.configRepository.findOne({
2265
- where: { id: createDto.integration_config_id },
2266
- });
2267
-
2268
- if (!config) {
2269
- failed.push({
2270
- data: createDto,
2271
- error: 'Integration configuration not found',
2272
- });
2273
- continue;
2274
- }
2275
-
2276
- // Create new mapping
2277
- const userIntegration =
2278
- this.userIntegrationRepository.create(createDto);
2279
- const savedIntegration =
2280
- await this.userIntegrationRepository.save(userIntegration);
2281
- created.push(savedIntegration);
2282
-
2283
- this.logger.log(
2284
- `Created user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`,
2285
- );
2286
- }
2287
- } catch (error) {
2288
- failed.push({
2289
- data: createDto,
2290
- error: error.message || 'Unknown error',
2291
- });
2292
- this.logger.error(
2293
- `Error processing user integration for user ${createDto.user_id}: ${error.message}`,
2294
- );
2295
- }
2296
- }
2297
-
2298
- const summary = {
2299
- total: bulkDto.user_integrations.length,
2300
- created: created.length,
2301
- updated: updated.length,
2302
- failed: failed.length,
2303
- };
2304
-
2305
- this.logger.log(
2306
- `Bulk user integration completed: ${summary.created} created, ${summary.updated} updated, ${summary.failed} failed`,
2307
- );
2308
-
2309
- return { created, updated, failed, summary };
2310
- } catch (error) {
2311
- this.logger.error(
2312
- `Error in bulk user integration creation: ${error.message}`,
2313
- );
2314
- throw error;
2315
- }
2316
- }
2317
-
2318
- async getUserIntegrations(userId: number): Promise<UserIntegration[]> {
2319
- try {
2320
- return await this.userIntegrationRepository.find({
2321
- where: { user_id: userId, is_active: true },
2322
- order: { created_at: 'DESC' },
2323
- });
2324
- } catch (error) {
2325
- this.logger.error(
2326
- `Error fetching user integrations for user ${userId}: ${error.message}`,
2327
- );
2328
- throw error;
2329
- }
2330
- }
2331
-
2332
- async getConfigUserIntegrations(
2333
- configId: number,
2334
- ): Promise<UserIntegration[]> {
2335
- try {
2336
- return await this.userIntegrationRepository.find({
2337
- where: { integration_config_id: configId, is_active: true },
2338
- order: { created_at: 'DESC' },
2339
- });
2340
- } catch (error) {
2341
- this.logger.error(
2342
- `Error fetching user integrations for config ${configId}: ${error.message}`,
2343
- );
2344
- throw error;
2345
- }
2346
- }
2347
-
2348
- async getUserIntegrationByUserAndConfig(
2349
- userId: number,
2350
- configId: number,
2351
- ): Promise<UserIntegration | null> {
2352
- try {
2353
- return await this.userIntegrationRepository.findOne({
2354
- where: {
2355
- user_id: userId,
2356
- integration_config_id: configId,
2357
- is_active: true,
2358
- },
2359
- });
2360
- } catch (error) {
2361
- this.logger.error(
2362
- `Error fetching user integration for user ${userId} and config ${configId}: ${error.message}`,
2363
- );
2364
- throw error;
2365
- }
2366
- }
2367
-
2368
- async updateUserIntegration(
2369
- id: number,
2370
- updateDto: UpdateUserIntegrationDto,
2371
- ): Promise<UserIntegration> {
2372
- try {
2373
- const userIntegration = await this.userIntegrationRepository.findOne({
2374
- where: { id },
2375
- });
2376
-
2377
- if (!userIntegration) {
2378
- throw new Error('User integration mapping not found');
2379
- }
2380
-
2381
- Object.assign(userIntegration, updateDto);
2382
- return await this.userIntegrationRepository.save(userIntegration);
2383
- } catch (error) {
2384
- this.logger.error(
2385
- `Error updating user integration ${id}: ${error.message}`,
2386
- );
2387
- throw error;
2388
- }
2389
- }
2390
-
2391
- async deleteUserIntegration(id: number): Promise<void> {
2392
- try {
2393
- const userIntegration = await this.userIntegrationRepository.findOne({
2394
- where: { id },
2395
- });
2396
-
2397
- if (!userIntegration) {
2398
- throw new Error('User integration mapping not found');
2399
- }
2400
-
2401
- await this.userIntegrationRepository.remove(userIntegration);
2402
- } catch (error) {
2403
- this.logger.error(
2404
- `Error deleting user integration ${id}: ${error.message}`,
2405
- );
2406
- throw error;
2407
- }
2408
- }
2409
-
2410
- /**
2411
- * Get user integration data for a specific integration strategy
2412
- * This method is intended to be used by strategies that need user mapping
2413
- */
2414
- async getUserIntegrationForStrategy(
2415
- userId: number,
2416
- integrationConfigId: number,
2417
- ): Promise<any | null> {
2418
- try {
2419
- const userIntegration = await this.getUserIntegrationByUserAndConfig(
2420
- userId,
2421
- integrationConfigId,
2422
- );
2423
-
2424
- if (!userIntegration) {
2425
- return null;
2426
- }
2427
-
2428
- return {
2429
- external_user_id: userIntegration.external_user_id,
2430
- external_user_data: userIntegration.external_user_data,
2431
- };
2432
- } catch (error) {
2433
- this.logger.error(
2434
- `Error fetching user integration data for strategy: ${error.message}`,
2435
- );
2436
- return null;
2437
- }
2438
- }
2439
-
2440
- /**
2441
- * Check agent status for TELEPHONE integration
2442
- * Works with any provider that implements checkAgentStatus method
2443
- */
2444
- async checkAgentStatus(
2445
- levelId: number,
2446
- levelType: string,
2447
- appCode: string,
2448
- userId: number,
2449
- integrationType: 'TELEPHONE' = 'TELEPHONE',
2450
- ): Promise<{
2451
- success: boolean;
2452
- isReady?: boolean;
2453
- state?: string;
2454
- agentInfo?: any;
2455
- error?: string;
2456
- }> {
2457
- try {
2458
- // Get the active configuration
2459
- const config = await this.getSingleActiveConfig(
2460
- levelId,
2461
- levelType,
2462
- appCode,
2463
- integrationType,
2464
- );
2465
-
2466
- if (!config) {
2467
- return {
2468
- success: false,
2469
- error: 'No active TELEPHONE configuration found',
2470
- };
2471
- }
2472
-
2473
- // Get user integration mapping
2474
- const userIntegration = await this.getUserIntegrationByUserAndConfig(
2475
- userId,
2476
- config.id,
2477
- );
2478
-
2479
- if (!userIntegration) {
2480
- return {
2481
- success: false,
2482
- error:
2483
- 'User integration mapping not found. Please configure agent details.',
2484
- };
2485
- }
2486
-
2487
- // Create strategy instance
2488
- const strategy = this.integrationFactory.create(
2489
- config.integration_type,
2490
- 'API', // service is deprecated, using default
2491
- config.integration_provider,
2492
- );
2493
-
2494
- // Get the merged config with user data
2495
- let mergedConfig = config.config_json;
2496
-
2497
- if (strategy.createUserSpecificConfig) {
2498
- mergedConfig = strategy.createUserSpecificConfig(
2499
- config.config_json,
2500
- userIntegration.external_user_id,
2501
- );
2502
- }
2503
-
2504
- // Check if strategy supports agent status check
2505
- if (typeof (strategy as any).checkAgentStatus !== 'function') {
2506
- return {
2507
- success: false,
2508
- error: `Agent status check not supported for provider: ${config.integration_provider}`,
2509
- };
2510
- }
2511
-
2512
- // Check agent status
2513
- const statusResult = await (strategy as any).checkAgentStatus(
2514
- mergedConfig,
2515
- );
2516
-
2517
- return {
2518
- success: true,
2519
- isReady: statusResult.isReady,
2520
- state: statusResult.state,
2521
- error: statusResult.error,
2522
- };
2523
- } catch (error) {
2524
- this.logger.error(
2525
- `Error checking agent status: ${error.message}`,
2526
- error.stack,
2527
- );
2528
- return {
2529
- success: false,
2530
- error: error.message || 'Failed to check agent status',
2531
- };
2532
- }
2533
- }
2534
- }
1
+ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
2
+ import { InjectRepository } from '@nestjs/typeorm';
3
+ import { DataSource, Not, Repository } from 'typeorm';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { google } from 'googleapis';
6
+ import { IntegrationConfig } from '../entity/integration-config.entity';
7
+ import { UserIntegration } from '../entity/user-integration.entity';
8
+ import { IntegrationEntityMapper } from '../entity/integration-entity-mapper.entity';
9
+ import { IntegrationFactory } from '../factories/integration.factory';
10
+ import { IntegrationResult } from '../strategies/integration.strategy';
11
+ import { GmailApiStrategy } from '../strategies/email/gmail-api.strategy';
12
+ import { SendGridApiStrategy } from '../strategies/email/sendgrid-api.strategy';
13
+ import { IntegrationQueueService } from './integration-queue.service';
14
+ import {
15
+ BulkMessageDto,
16
+ BulkCreateUserIntegrationDto,
17
+ CreateUserIntegrationDto,
18
+ UpdateUserIntegrationDto,
19
+ } from '../dto/create-config.dto';
20
+ import { FieldMapperService } from '../../mapper/service/field-mapper.service';
21
+ import { COMM_TEMPLATE } from '../../../constant/global.constant';
22
+ import { EntityServiceImpl } from '../../meta/service/entity-service-impl.service';
23
+
24
+ export interface SendMessageDto {
25
+ levelId: number;
26
+ levelType: string;
27
+ app_code: string;
28
+ to: string;
29
+ message: string;
30
+ mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
31
+ priority?: number;
32
+ user_id?: number;
33
+ }
34
+
35
+ export interface GenericMessageDto {
36
+ levelId: number;
37
+ levelType: string;
38
+ app_code: string;
39
+ to: string | string[];
40
+ message: string;
41
+ subject?: string;
42
+ type?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
43
+ priority?: 'high' | 'medium' | 'low';
44
+ cc?: string | string[];
45
+ bcc?: string | string[];
46
+ html?: string;
47
+ attachments?: any[];
48
+ mediaUrl?: string;
49
+ templateId?: string;
50
+ variables?: Record<string, any>;
51
+ user_id?: number;
52
+ entity_type?: string;
53
+ entity_id?: number;
54
+ }
55
+
56
+ export interface IntegrationConfigWithConfig extends IntegrationConfig {
57
+ config?: any;
58
+ }
59
+
60
+ interface GmailOAuthState {
61
+ levelId: number;
62
+ levelType: string;
63
+ app_code: string;
64
+ email?: string;
65
+ timestamp: number;
66
+ }
67
+
68
+ export interface GmailSSOResult {
69
+ hubId: number;
70
+ configId: number;
71
+ }
72
+
73
+ @Injectable()
74
+ export class IntegrationService {
75
+ private readonly logger = new Logger(IntegrationService.name);
76
+ private readonly gmailOAuthStates = new Map<string, GmailOAuthState>();
77
+
78
+ constructor(
79
+ @InjectRepository(IntegrationConfig)
80
+ private readonly configRepository: Repository<IntegrationConfig>,
81
+ @InjectRepository(UserIntegration)
82
+ private readonly userIntegrationRepository: Repository<UserIntegration>,
83
+ @InjectRepository(IntegrationEntityMapper)
84
+ private readonly entityMapperRepository: Repository<IntegrationEntityMapper>,
85
+ private readonly dataSource: DataSource,
86
+ private readonly integrationFactory: IntegrationFactory,
87
+ private readonly gmailApiStrategy: GmailApiStrategy,
88
+ private readonly sendGridApiStrategy: SendGridApiStrategy,
89
+ private readonly configService: ConfigService,
90
+ @Inject('FieldMapperService') private readonly fieldMapperService: FieldMapperService,
91
+ private readonly entityService: EntityServiceImpl,
92
+ @Inject(forwardRef(() => IntegrationQueueService))
93
+ private readonly queueService?: IntegrationQueueService,
94
+ ) {
95
+ }
96
+
97
+ private deriveServiceType(
98
+ integration_type: string,
99
+ integration_provider: string,
100
+ config_json: any,
101
+ ): string {
102
+ // All integrations use API only (no SMTP support)
103
+ return 'API';
104
+ }
105
+
106
+ async sendMessage({
107
+ levelId,
108
+ levelType,
109
+ app_code,
110
+ to,
111
+ message,
112
+ mode,
113
+ priority = 1,
114
+ user_id,
115
+ }: SendMessageDto): Promise<IntegrationResult> {
116
+ try {
117
+ // Get active communication configs for the level
118
+ const configs = await this.getActiveConfigs(
119
+ levelId,
120
+ levelType,
121
+ app_code,
122
+ mode,
123
+ );
124
+
125
+ if (!configs.length) {
126
+ throw new Error(
127
+ `No active communication configuration found for ${levelType} ${levelId}`,
128
+ );
129
+ }
130
+
131
+ // Sort by priority if provided
132
+ const sortedConfigs = this.sortConfigsByPriority(configs, priority);
133
+
134
+ // Try each config until one succeeds
135
+ for (const config of sortedConfigs) {
136
+ try {
137
+ const result = await this.sendViaConfig(config, to, message, user_id);
138
+ if (result.success) {
139
+ this.logger.log(
140
+ `Message sent successfully via ${config.integration_provider}`,
141
+ );
142
+ return result;
143
+ }
144
+
145
+ this.logger.warn(
146
+ `Failed to send via ${config.integration_provider}: ${result.error}`,
147
+ );
148
+ } catch (error) {
149
+ this.logger.error(
150
+ `Error sending via ${config.integration_provider}:`,
151
+ error.message,
152
+ );
153
+ continue;
154
+ }
155
+ }
156
+
157
+ throw new Error('All communication providers failed');
158
+ } catch (error) {
159
+ this.logger.error('Communication service error:', error.message);
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ async getActiveConfigs(
165
+ levelId: number,
166
+ levelType: string,
167
+ app_code: string,
168
+ mode?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
169
+ ): Promise<IntegrationConfig[]> {
170
+ const queryBuilder = this.configRepository
171
+ .createQueryBuilder('config')
172
+ .where('config.level_id = :levelId', { levelId })
173
+ .andWhere('config.level_type = :levelType', { levelType })
174
+ .andWhere('config.app_code = :app_code', { app_code })
175
+ .andWhere('config.status = 1');
176
+
177
+ if (mode) {
178
+ queryBuilder.andWhere('config.integration_type = :mode', { mode });
179
+ }
180
+
181
+ return await queryBuilder.getMany();
182
+ }
183
+
184
+ async getSingleActiveConfig(
185
+ levelId: number,
186
+ levelType: string,
187
+ app_code: string,
188
+ integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
189
+ ): Promise<IntegrationConfig | null> {
190
+ const configs = await this.configRepository
191
+ .createQueryBuilder('config')
192
+ .where('config.level_id = :levelId', { levelId })
193
+ .andWhere('config.level_type = :levelType', { levelType })
194
+ .andWhere('config.app_code = :app_code', { app_code })
195
+ .andWhere('config.integration_type = :integration_type', { integration_type })
196
+ .andWhere('config.status = 1')
197
+ .orderBy('config.is_default', 'DESC')
198
+ .addOrderBy('config.priority', 'ASC')
199
+ .addOrderBy('config.created_at', 'DESC')
200
+ .getMany();
201
+
202
+ return configs.length > 0 ? configs[0] : null;
203
+ }
204
+
205
+ async getAllIntegrationData(
206
+ loggedInUser,
207
+ integration_type?: string,
208
+ ): Promise<any[]> {
209
+ try {
210
+ const allIntegrationData = await this.dataSource.query(
211
+ `SELECT *
212
+ FROM cr_integration_master`,
213
+ );
214
+
215
+ // if entityType is provided, filter the results
216
+ if (integration_type) {
217
+ return allIntegrationData.filter(
218
+ (data) =>
219
+ data.integration_type.toLowerCase() ===
220
+ integration_type.toLowerCase(),
221
+ );
222
+ }
223
+
224
+ return allIntegrationData;
225
+ } catch (error) {
226
+ this.logger.error('Error fetching integration data:', error.message);
227
+ return [];
228
+ }
229
+ }
230
+
231
+ private sortConfigsByPriority(
232
+ configs: IntegrationConfig[],
233
+ _priority: number,
234
+ ): IntegrationConfig[] {
235
+ return configs.sort((a, b) => {
236
+ // First sort by default (true comes first)
237
+ if (a.is_default !== b.is_default) {
238
+ return a.is_default ? -1 : 1;
239
+ }
240
+ // Then by priority (lower number = higher priority)
241
+ return a.priority - b.priority;
242
+ });
243
+ }
244
+
245
+ private async sendViaConfig(
246
+ config: IntegrationConfig,
247
+ to: string,
248
+ message: string,
249
+ user_id?: number,
250
+ ): Promise<IntegrationResult> {
251
+ const strategy = this.integrationFactory.create(
252
+ config.integration_type,
253
+ 'API', // service is deprecated, using default
254
+ config.integration_provider,
255
+ );
256
+
257
+ // Get user integration data if user_id provided
258
+ let finalConfig = config.config_json;
259
+ if (user_id) {
260
+ const userIntegrationData = await this.getUserIntegrationForStrategy(
261
+ user_id,
262
+ config.id,
263
+ );
264
+
265
+ if (userIntegrationData) {
266
+ finalConfig = {
267
+ ...config.config_json,
268
+ external_user_id: userIntegrationData.external_user_id,
269
+ };
270
+ }
271
+ }
272
+
273
+ const result = await strategy.sendMessage(to, message, finalConfig);
274
+
275
+ // If token was refreshed, update it in the database
276
+ if (result.refreshedToken && result.success) {
277
+ try {
278
+ const currentConfig = config.config_json as any;
279
+ const updatedConfig = {
280
+ ...currentConfig,
281
+ accessToken: result.refreshedToken,
282
+ };
283
+
284
+ await this.configRepository.update(config.id, {
285
+ config_json: updatedConfig,
286
+ } as any);
287
+
288
+ this.logger.log(
289
+ `Updated access token for ${config.integration_provider} configuration`,
290
+ );
291
+ } catch (error) {
292
+ this.logger.warn(
293
+ `Failed to update refreshed token in database: ${error.message}`,
294
+ );
295
+ }
296
+ }
297
+
298
+ return result;
299
+ }
300
+
301
+ async createIntegrationConfig(
302
+ levelId: number,
303
+ levelType: string,
304
+ app_code: string,
305
+ configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
306
+ provider: string,
307
+ integration_source_id: number,
308
+ config: any,
309
+ priority?: number,
310
+ is_default?: boolean,
311
+ ): Promise<
312
+ IntegrationConfig | { authUrl: string; state: string; message: string }
313
+ > {
314
+ // Validate that no duplicate provider configurations exist
315
+ await this.validateUniqueActiveConfig(
316
+ levelId,
317
+ levelType,
318
+ app_code,
319
+ configType,
320
+ provider,
321
+ );
322
+
323
+ // Deactivate all other configurations of the same integration type
324
+ await this.configRepository.update(
325
+ {
326
+ level_id: levelId,
327
+ level_type: levelType,
328
+ app_code: app_code,
329
+ integration_type: configType,
330
+ status: 1,
331
+ },
332
+ { status: 0 },
333
+ );
334
+
335
+ // Validate provider and get service type from supported combinations
336
+ const supportedCombinations = await this.getSupportedCombinations();
337
+ const validCombination = supportedCombinations.find(
338
+ (combo) =>
339
+ combo.mode === configType &&
340
+ combo.provider.toLowerCase() === provider.toLowerCase(),
341
+ );
342
+
343
+ if (!validCombination) {
344
+ throw new Error(`Unsupported combination: ${configType}/${provider}`);
345
+ }
346
+
347
+ const service = validCombination.service;
348
+
349
+ // Check if this requires OAuth flow
350
+ const requiresOAuth = this.requiresOAuthFlow(configType, provider, config);
351
+
352
+ if (requiresOAuth) {
353
+ // Generate OAuth URL and return it instead of creating config immediately
354
+ return await this.generateOAuthUrl(
355
+ levelId,
356
+ levelType,
357
+ app_code,
358
+ configType,
359
+ provider,
360
+ integration_source_id,
361
+ config,
362
+ priority,
363
+ is_default,
364
+ );
365
+ }
366
+
367
+ // Direct config creation (non-OAuth flow)
368
+ return await this.createDirectConfig(
369
+ levelId,
370
+ levelType,
371
+ app_code,
372
+ configType,
373
+ provider,
374
+ integration_source_id,
375
+ config,
376
+ priority,
377
+ is_default,
378
+ );
379
+ }
380
+
381
+ getSupportedCombinations(): {
382
+ mode: string;
383
+ service: string;
384
+ provider: string;
385
+ }[] {
386
+ return this.integrationFactory.getAllSupportedCombinations();
387
+ }
388
+
389
+ async getLevelConfigs(
390
+ levelId: number,
391
+ levelType: string,
392
+ filters?: {
393
+ app_code?: string;
394
+ integration_type?: 'WA' | 'SMS' | 'EMAIL' | 'TELEPHONE';
395
+ integration_provider?: string;
396
+ },
397
+ ): Promise<
398
+ Array<IntegrationConfig & { linkedSource?: string; configDetails?: any }>
399
+ > {
400
+ const where: any = { level_id: levelId, level_type: levelType };
401
+
402
+ if (filters?.app_code) {
403
+ where.app_code = filters.app_code;
404
+ }
405
+
406
+ if (filters?.integration_type) {
407
+ where.integration_type = filters.integration_type;
408
+ }
409
+
410
+ if (filters?.integration_provider) {
411
+ where.integration_provider = filters.integration_provider;
412
+ }
413
+
414
+ const hubs = await this.configRepository.find({
415
+ where,
416
+ order: { created_at: 'DESC' },
417
+ });
418
+
419
+ // Enhance hubs with linked source information
420
+ const enhancedHubs = await Promise.all(
421
+ hubs.map(async (hub) => {
422
+ try {
423
+ const relatedConfig = await this.configRepository.findOne({
424
+ where: { id: hub.id },
425
+ });
426
+
427
+ if (!relatedConfig) {
428
+ return {
429
+ ...hub,
430
+ linkedSource: 'Configuration not found',
431
+ configDetails: null,
432
+ };
433
+ }
434
+
435
+ const linkedSource = this.extractLinkedSource(
436
+ hub.integration_type,
437
+ hub.integration_provider,
438
+ relatedConfig.config_json,
439
+ );
440
+
441
+ const configDetails = this.extractConfigDetails(
442
+ hub.integration_type,
443
+ hub.integration_provider,
444
+ relatedConfig.config_json,
445
+ );
446
+
447
+ return {
448
+ ...hub,
449
+ linkedSource,
450
+ configDetails,
451
+ };
452
+ } catch (error) {
453
+ this.logger.warn(
454
+ `Error extracting linked source for hub ${hub.id}:`,
455
+ error.message,
456
+ );
457
+ return {
458
+ ...hub,
459
+ linkedSource: 'Error retrieving source',
460
+ configDetails: null,
461
+ };
462
+ }
463
+ }),
464
+ );
465
+
466
+ return enhancedHubs;
467
+ }
468
+
469
+ private extractLinkedSource(
470
+ configType: string,
471
+ provider: string,
472
+ configJson: any,
473
+ ): string {
474
+ const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
475
+
476
+ try {
477
+ switch (key) {
478
+ // Gmail configurations
479
+ case 'email_gmail':
480
+ case 'email_smtp_gmail':
481
+ return configJson?.email || 'Gmail account not configured';
482
+
483
+ // Outlook configurations
484
+ case 'email_outlook':
485
+ case 'email_smtp_outlook':
486
+ return configJson?.email || 'Outlook account not configured';
487
+
488
+ // WhatsApp configurations
489
+ case 'wa_api_whatsapp':
490
+ return configJson?.phoneNumberId
491
+ ? `WhatsApp Business: ${configJson.phoneNumberId}`
492
+ : 'WhatsApp not configured';
493
+
494
+ // SMS configurations
495
+ case 'sms_third_party_twilio':
496
+ return configJson?.fromNumber
497
+ ? `Twilio: ${configJson.fromNumber}`
498
+ : 'Twilio number not configured';
499
+
500
+ case 'sms_third_party_knowlarity':
501
+ return configJson?.callerNumber
502
+ ? `Knowlarity: ${configJson.callerNumber}`
503
+ : 'Knowlarity number not configured';
504
+
505
+ // Telephone configurations
506
+ case 'telephone_third_party_knowlarity':
507
+ return configJson?.callerNumber
508
+ ? `Knowlarity Voice: ${configJson.callerNumber}`
509
+ : 'Knowlarity voice number not configured';
510
+
511
+ case 'telephone_third_party_ozonetel':
512
+ return configJson?.userName
513
+ ? `Ozonetel Voice: ${configJson.userName}`
514
+ : 'Ozonetel voice not configured';
515
+
516
+ // AWS SES configurations
517
+ case 'email_aws-ses':
518
+ case 'email_ses':
519
+ return configJson?.fromEmail || 'AWS SES not configured';
520
+
521
+ // SendGrid configurations
522
+ case 'email_smtp_sendgrid':
523
+ return configJson?.from || 'SendGrid not configured';
524
+
525
+ // Generic SMTP configurations
526
+ case 'email_smtp_custom':
527
+ case 'email_smtp_generic':
528
+ return configJson?.from || configJson?.user || 'SMTP not configured';
529
+
530
+ default:
531
+ // Generic fallback - try to find common identifier fields
532
+ if (configJson?.email) return configJson.email;
533
+ if (configJson?.from) return configJson.from;
534
+ if (configJson?.user) return configJson.user;
535
+ if (configJson?.phoneNumberId) return configJson.phoneNumberId;
536
+ if (configJson?.fromNumber) return configJson.fromNumber;
537
+ if (configJson?.callerNumber) return configJson.callerNumber;
538
+
539
+ return `${provider} configured`;
540
+ }
541
+ } catch (error) {
542
+ return 'Configuration error';
543
+ }
544
+ }
545
+
546
+ private extractConfigDetails(
547
+ configType: string,
548
+ provider: string,
549
+ configJson: any,
550
+ ): any {
551
+ const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
552
+
553
+ try {
554
+ switch (key) {
555
+ // Gmail configurations
556
+ case 'email_gmail':
557
+ return {
558
+ email: configJson?.email,
559
+ authMethod: configJson?.authMethod || 'OAUTH2',
560
+ hasRefreshToken: !!configJson?.refreshToken,
561
+ isExpired: configJson?.expiryDate
562
+ ? new Date(configJson.expiryDate) < new Date()
563
+ : false,
564
+ };
565
+
566
+ case 'email_smtp_gmail':
567
+ return {
568
+ email: configJson?.email,
569
+ authMethod: 'SMTP',
570
+ hasPassword: !!configJson?.password,
571
+ };
572
+
573
+ // Outlook configurations
574
+ case 'email_outlook':
575
+ return {
576
+ email: configJson?.email,
577
+ authMethod: 'OAUTH2',
578
+ hasRefreshToken: !!configJson?.refreshToken,
579
+ tenantId: configJson?.tenantId,
580
+ };
581
+
582
+ case 'email_smtp_outlook':
583
+ return {
584
+ email: configJson?.email,
585
+ authMethod: 'SMTP',
586
+ hasPassword: !!configJson?.password,
587
+ };
588
+
589
+ // WhatsApp configurations
590
+ case 'wa_api_whatsapp':
591
+ return {
592
+ phoneNumberId: configJson?.phoneNumberId,
593
+ apiVersion: configJson?.apiVersion || 'v17.0',
594
+ hasAccessToken: !!configJson?.accessToken,
595
+ };
596
+
597
+ // SMS configurations
598
+ case 'sms_third_party_twilio':
599
+ return {
600
+ fromNumber: configJson?.fromNumber,
601
+ accountSid: configJson?.accountSid
602
+ ? configJson.accountSid.substring(0, 8) + '...'
603
+ : null,
604
+ hasAuthToken: !!configJson?.authToken,
605
+ };
606
+
607
+ case 'sms_third_party_knowlarity':
608
+ return {
609
+ callerNumber: configJson?.callerNumber,
610
+ callType: configJson?.callType || 'sms',
611
+ hasApiSecret: !!configJson?.apiSecret,
612
+ };
613
+
614
+ // Telephone configurations
615
+ case 'telephone_third_party_knowlarity':
616
+ return {
617
+ callerNumber: configJson?.callerNumber,
618
+ callType: configJson?.callType || 'voice',
619
+ language: configJson?.language || 'en',
620
+ hasApiSecret: !!configJson?.apiSecret,
621
+ };
622
+
623
+ case 'telephone_third_party_ozonetel':
624
+ return {
625
+ userName: configJson?.userName,
626
+ agentID: configJson?.agentID,
627
+ campaignName: configJson?.campaignName,
628
+ agentLoginUrl: configJson?.agentLoginUrl,
629
+ hasApiKey: !!configJson?.apiKey,
630
+ };
631
+
632
+ // AWS SES configurations
633
+ case 'email_aws-ses':
634
+ case 'email_ses':
635
+ return {
636
+ fromEmail: configJson?.fromEmail,
637
+ region: configJson?.region,
638
+ hasCredentials: !!(
639
+ configJson?.accessKeyId && configJson?.secretAccessKey
640
+ ),
641
+ };
642
+
643
+ // SendGrid configurations
644
+ case 'email_smtp_sendgrid':
645
+ return {
646
+ from: configJson?.from,
647
+ hasApiKey: !!configJson?.apiKey,
648
+ templateId: configJson?.templateId,
649
+ };
650
+
651
+ // Generic SMTP configurations
652
+ case 'email_smtp_custom':
653
+ case 'email_smtp_generic':
654
+ return {
655
+ host: configJson?.host,
656
+ port: configJson?.port || 587,
657
+ secure: configJson?.secure || false,
658
+ from: configJson?.from || configJson?.user,
659
+ hasCredentials: !!(configJson?.user && configJson?.password),
660
+ };
661
+
662
+ default: {
663
+ // Generic details - return safe subset of config
664
+ const safeConfig: any = {};
665
+
666
+ // Include non-sensitive fields
667
+ const safeFields = [
668
+ 'email',
669
+ 'from',
670
+ 'user',
671
+ 'host',
672
+ 'port',
673
+ 'secure',
674
+ 'phoneNumberId',
675
+ 'fromNumber',
676
+ 'callerNumber',
677
+ 'region',
678
+ 'apiVersion',
679
+ 'callType',
680
+ 'language',
681
+ 'templateId',
682
+ ];
683
+
684
+ safeFields.forEach((field) => {
685
+ if (configJson?.[field]) {
686
+ safeConfig[field] = configJson[field];
687
+ }
688
+ });
689
+
690
+ // Include boolean indicators for sensitive fields
691
+ const sensitiveFields = [
692
+ 'accessToken',
693
+ 'refreshToken',
694
+ 'password',
695
+ 'authToken',
696
+ 'apiKey',
697
+ 'apiSecret',
698
+ 'clientSecret',
699
+ 'secretAccessKey',
700
+ ];
701
+
702
+ sensitiveFields.forEach((field) => {
703
+ if (configJson?.[field]) {
704
+ safeConfig[
705
+ `has${field.charAt(0).toUpperCase() + field.slice(1)}`
706
+ ] = true;
707
+ }
708
+ });
709
+
710
+ return safeConfig;
711
+ }
712
+ }
713
+ } catch (error) {
714
+ return { error: 'Unable to extract configuration details' };
715
+ }
716
+ }
717
+
718
+ async updateConfigStatus(hubId: number, status: number): Promise<void> {
719
+ // Find the hub to get level and type information
720
+ const config = await this.configRepository.findOne({
721
+ where: { id: hubId },
722
+ });
723
+
724
+ if (!config) {
725
+ throw new Error('Integration configuration not found');
726
+ }
727
+
728
+ // If activating, deactivate ALL other configs of the same integration type
729
+ if (status === 1) {
730
+ await this.configRepository.update(
731
+ {
732
+ level_id: config.level_id,
733
+ level_type: config.level_type,
734
+ app_code: config.app_code,
735
+ integration_type: config.integration_type,
736
+ status: 1,
737
+ id: Not(hubId),
738
+ },
739
+ { status: 0 },
740
+ );
741
+ }
742
+
743
+ // Update the requested config
744
+ await this.configRepository.update(hubId, { status });
745
+ }
746
+
747
+ async deleteConfiguration(configId: number): Promise<void> {
748
+ // Find the hub to get the id
749
+ const config = await this.configRepository.findOne({
750
+ where: { id: configId },
751
+ });
752
+
753
+ if (!config) {
754
+ throw new Error('Integration configuration not found');
755
+ }
756
+
757
+ await this.configRepository.delete(configId);
758
+
759
+ this.logger.log(`Integration configuration deleted: ${configId}`);
760
+ }
761
+
762
+ async updateConfiguration(
763
+ hubId: number,
764
+ updateData: {
765
+ config?: any;
766
+ priority?: number;
767
+ is_default?: boolean;
768
+ status?: number;
769
+ },
770
+ ): Promise<IntegrationConfig & { config?: any }> {
771
+ // Find the existing hub
772
+ const config = await this.configRepository.findOne({
773
+ where: { id: hubId },
774
+ });
775
+
776
+ if (!config) {
777
+ throw new Error('Integration configuration not found');
778
+ }
779
+
780
+ // Update configuration JSON if provided
781
+ if (updateData.config) {
782
+ // Merge the new config with existing config
783
+ const updatedConfigJson = {
784
+ ...config.config_json,
785
+ ...updateData.config,
786
+ };
787
+
788
+ await this.configRepository.update(config.id, {
789
+ config_json: updatedConfigJson,
790
+ } as any);
791
+ }
792
+
793
+ // Handle default configuration logic (simplified for now)
794
+ if (updateData.is_default === true) {
795
+ // Remove default from other configurations of same type and level
796
+ await this.configRepository.update(
797
+ {
798
+ level_id: config.level_id,
799
+ level_type: config.level_type,
800
+ integration_type: config.integration_type,
801
+ id: Not(hubId),
802
+ },
803
+ { is_default: false },
804
+ );
805
+ }
806
+
807
+ // Apply direct config updates if any
808
+ const directUpdates: any = {};
809
+ if (updateData.priority !== undefined)
810
+ directUpdates.priority = updateData.priority;
811
+ if (updateData.is_default !== undefined)
812
+ directUpdates.is_default = updateData.is_default;
813
+ if (updateData.status !== undefined)
814
+ directUpdates.status = updateData.status;
815
+
816
+ if (Object.keys(directUpdates).length > 0) {
817
+ await this.configRepository.update(config.id, directUpdates);
818
+ }
819
+
820
+ // Fetch and return updated config
821
+ const updatedConfig = await this.configRepository.findOne({
822
+ where: { id: hubId },
823
+ });
824
+
825
+ return {
826
+ ...updatedConfig,
827
+ config: updatedConfig?.config_json,
828
+ } as any;
829
+ }
830
+
831
+ async sendGenericMessage(
832
+ messageDto: GenericMessageDto,
833
+ ): Promise<IntegrationResult> {
834
+ try {
835
+ const {
836
+ levelId,
837
+ levelType,
838
+ app_code,
839
+ to,
840
+ message,
841
+ type,
842
+ priority = 'medium',
843
+ html,
844
+ cc,
845
+ bcc,
846
+ attachments,
847
+ mediaUrl,
848
+ templateId,
849
+ user_id,
850
+ entity_id,
851
+ entity_type,
852
+ } = messageDto;
853
+
854
+ let subject = messageDto.subject;
855
+
856
+ // Auto-detect communication type if not specified
857
+ const communicationType = this.detectCommunicationType(to, type);
858
+
859
+ // Get active configs for the detected type
860
+ const configs = await this.getActiveConfigs(
861
+ levelId,
862
+ levelType,
863
+ app_code,
864
+ communicationType,
865
+ );
866
+
867
+ if (!configs.length) {
868
+ this.logger.warn(
869
+ `No communication hubs found for ${levelType} ${levelId}. Please configure integration providers using the createIntegrationConfig method.`,
870
+ );
871
+ throw new Error(
872
+ `No active ${communicationType} configuration found for ${levelType} ${levelId}. Please configure a communication provider first.`,
873
+ );
874
+ }
875
+
876
+ // Sort hubs by priority and default preference
877
+ const sortedConfigs = this.sortConfigsByPriorityAndDefault(
878
+ configs,
879
+ priority,
880
+ );
881
+
882
+ // Prepare enhanced config with additional parameters
883
+ const enhancedConfig = {
884
+ subject,
885
+ html,
886
+ cc,
887
+ bcc,
888
+ attachments,
889
+ mediaUrl,
890
+ };
891
+
892
+ // Handle multiple recipients by sending to each individually
893
+ if (Array.isArray(to)) {
894
+ const results: IntegrationResult[] = [];
895
+ let hasSuccess = false;
896
+
897
+ for (const recipient of to) {
898
+ for (const hub of sortedConfigs) {
899
+ try {
900
+ const serviceType = this.deriveServiceType(
901
+ hub.integration_type,
902
+ hub.integration_provider,
903
+ hub.config_json,
904
+ );
905
+ const strategy = this.integrationFactory.create(
906
+ hub.integration_type,
907
+ serviceType,
908
+ hub.integration_provider,
909
+ );
910
+
911
+ // Process template if provided
912
+ let variables;
913
+ let richText;
914
+ let externalTemplateId: string | undefined = undefined;
915
+ if (templateId) {
916
+ const templateData = await this.processTemplate(
917
+ parseInt(templateId, 10),
918
+ entity_type,
919
+ entity_id,
920
+ );
921
+ variables = templateData.variables;
922
+ externalTemplateId = templateData.externalTemplateId;
923
+ richText = templateData.rich_text;
924
+ subject = templateData.subject;
925
+ }
926
+
927
+ // Merge config with enhanced parameters
928
+ let finalConfig = {
929
+ ...hub.config_json,
930
+ ...enhancedConfig,
931
+ templateId: externalTemplateId,
932
+ variables,
933
+ richText,
934
+ };
935
+
936
+ // Handle user integration if user_id provided
937
+ if (user_id) {
938
+ const userIntegrationData =
939
+ await this.getUserIntegrationForStrategy(user_id, hub.id);
940
+
941
+ if (userIntegrationData) {
942
+ finalConfig = {
943
+ ...finalConfig,
944
+ external_user_id: userIntegrationData.external_user_id,
945
+ };
946
+ }
947
+ }
948
+
949
+ const result = await strategy.sendMessage(
950
+ recipient,
951
+ message,
952
+ finalConfig,
953
+ );
954
+
955
+ if (result.success) {
956
+ this.logger.log(
957
+ `Generic message sent successfully via ${hub.integration_provider} to ${recipient}`,
958
+ );
959
+ results.push(result);
960
+ hasSuccess = true;
961
+ break; // Move to next recipient
962
+ }
963
+
964
+ this.logger.warn(
965
+ `Failed to send via ${hub.integration_provider} to ${recipient}: ${result.error}`,
966
+ );
967
+ } catch (error) {
968
+ this.logger.error(
969
+ `Error sending via ${hub.integration_provider} to ${recipient}:`,
970
+ error.message,
971
+ );
972
+ continue;
973
+ }
974
+ }
975
+ }
976
+
977
+ if (hasSuccess) {
978
+ return {
979
+ success: true,
980
+ messageId: results.map((r) => r.messageId).join(','),
981
+ provider: 'multiple',
982
+ service: 'multiple',
983
+ timestamp: new Date(),
984
+ };
985
+ }
986
+ } else {
987
+ // Handle single recipient
988
+ for (const hub of sortedConfigs) {
989
+ try {
990
+ const serviceType = this.deriveServiceType(
991
+ hub.integration_type,
992
+ hub.integration_provider,
993
+ hub.config_json,
994
+ );
995
+ const strategy = this.integrationFactory.create(
996
+ hub.integration_type,
997
+ serviceType,
998
+ hub.integration_provider,
999
+ );
1000
+
1001
+ // Process template if provided
1002
+ let variables;
1003
+ let externalTemplateId: string | undefined = undefined;
1004
+ let richText;
1005
+ if (templateId) {
1006
+ const templateData = await this.processTemplate(
1007
+ parseInt(templateId, 10),
1008
+ entity_type,
1009
+ entity_id,
1010
+ );
1011
+ variables = templateData.variables;
1012
+ externalTemplateId = templateData.externalTemplateId;
1013
+ richText = templateData.rich_text;
1014
+ subject = templateData.subject;
1015
+ }
1016
+
1017
+ // Merge config with enhanced parameters
1018
+ let finalConfig = {
1019
+ ...hub.config_json,
1020
+ ...enhancedConfig,
1021
+ templateId: externalTemplateId,
1022
+ variables,
1023
+ richText
1024
+ };
1025
+
1026
+ // Handle user integration if user_id provided
1027
+ if (user_id) {
1028
+ const userIntegrationData =
1029
+ await this.getUserIntegrationForStrategy(user_id, hub.id);
1030
+
1031
+ if (userIntegrationData) {
1032
+ finalConfig = {
1033
+ ...finalConfig,
1034
+ external_user_id: userIntegrationData.external_user_id,
1035
+ };
1036
+ }
1037
+ }
1038
+
1039
+ const result = await strategy.sendMessage(
1040
+ to,
1041
+ message,
1042
+ finalConfig,
1043
+ );
1044
+
1045
+ if (result.success) {
1046
+ this.logger.log(
1047
+ `Generic message sent successfully via ${hub.integration_provider} to ${to}`,
1048
+ );
1049
+ return result;
1050
+ }
1051
+
1052
+ this.logger.warn(
1053
+ `Failed to send via ${hub.integration_provider}: ${result.error}`,
1054
+ );
1055
+ } catch (error) {
1056
+ this.logger.error(
1057
+ `Error sending via ${hub.integration_provider}:`,
1058
+ error.message,
1059
+ );
1060
+ continue;
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ throw new Error('All communication providers failed');
1066
+ } catch (error) {
1067
+ this.logger.error('Generic communication service error:', error.message);
1068
+ throw error;
1069
+ }
1070
+ }
1071
+
1072
+ async sendBulkMessage(
1073
+ bulkDto: BulkMessageDto,
1074
+ ): Promise<{ results: IntegrationResult[]; summary: any }> {
1075
+ try {
1076
+ const {
1077
+ levelId,
1078
+ levelType,
1079
+ app_code,
1080
+ recipients,
1081
+ message,
1082
+ type,
1083
+ priority = 'low',
1084
+ subject,
1085
+ html,
1086
+ templateId,
1087
+ variables,
1088
+ batchSize = 10,
1089
+ } = bulkDto;
1090
+
1091
+ const results: IntegrationResult[] = [];
1092
+ const batches = this.chunkArray(recipients, batchSize);
1093
+
1094
+ for (let i = 0; i < batches.length; i++) {
1095
+ const batch = batches[i];
1096
+ this.logger.log(
1097
+ `Processing batch ${i + 1}/${batches.length} with ${batch.length} recipients`,
1098
+ );
1099
+
1100
+ const batchPromises = batch.map(async (recipient) => {
1101
+ try {
1102
+ const messageDto: GenericMessageDto = {
1103
+ levelId,
1104
+ levelType,
1105
+ app_code,
1106
+ to: recipient,
1107
+ message,
1108
+ type,
1109
+ priority,
1110
+ subject,
1111
+ html,
1112
+ templateId,
1113
+ variables,
1114
+ };
1115
+
1116
+ return await this.sendGenericMessage(messageDto);
1117
+ } catch (error) {
1118
+ return {
1119
+ success: false,
1120
+ provider: 'unknown',
1121
+ service: 'unknown',
1122
+ error: error.message,
1123
+ timestamp: new Date(),
1124
+ } as IntegrationResult;
1125
+ }
1126
+ });
1127
+
1128
+ const batchResults = await Promise.allSettled(batchPromises);
1129
+ const processedResults = batchResults.map((result) => {
1130
+ if (result.status === 'fulfilled') {
1131
+ return result.value;
1132
+ } else {
1133
+ return {
1134
+ success: false,
1135
+ provider: 'unknown',
1136
+ service: 'unknown',
1137
+ error: result.reason?.message || 'Unknown error',
1138
+ timestamp: new Date(),
1139
+ } as IntegrationResult;
1140
+ }
1141
+ });
1142
+
1143
+ results.push(...processedResults);
1144
+
1145
+ // Rate limiting delay between batches
1146
+ if (i < batches.length - 1) {
1147
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1148
+ }
1149
+ }
1150
+
1151
+ const summary = {
1152
+ total: results.length,
1153
+ successful: results.filter((r) => r.success).length,
1154
+ failed: results.filter((r) => !r.success).length,
1155
+ successRate:
1156
+ (
1157
+ (results.filter((r) => r.success).length / results.length) *
1158
+ 100
1159
+ ).toFixed(2) + '%',
1160
+ };
1161
+
1162
+ this.logger.log(
1163
+ `Bulk message completed: ${summary.successful}/${summary.total} successful`,
1164
+ );
1165
+
1166
+ return { results, summary };
1167
+ } catch (error) {
1168
+ this.logger.error('Bulk communication service error:', error.message);
1169
+ throw error;
1170
+ }
1171
+ }
1172
+
1173
+ async scheduleMessage(
1174
+ scheduledDto: any,
1175
+ ): Promise<{ scheduled: boolean; scheduleId?: string }> {
1176
+ // For now, return a placeholder. In production, integrate with a job queue like Bull or Agenda
1177
+ this.logger.log(`Message scheduled for ${scheduledDto.scheduleFor}`);
1178
+
1179
+ return {
1180
+ scheduled: true,
1181
+ scheduleId: `sched_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
1182
+ };
1183
+ }
1184
+
1185
+ async sendTemplateMessage(templateDto: any): Promise<IntegrationResult> {
1186
+ // Get template content (integrate with your template system)
1187
+ const template = await this.getTemplate(templateDto.templateId);
1188
+
1189
+ const processedMessage = this.replaceTemplateVariables(
1190
+ template.content,
1191
+ templateDto.variables,
1192
+ );
1193
+ const processedSubject = template.subject
1194
+ ? this.replaceTemplateVariables(template.subject, templateDto.variables)
1195
+ : undefined;
1196
+
1197
+ const messageDto: GenericMessageDto = {
1198
+ levelId: templateDto.levelId,
1199
+ levelType: templateDto.levelType,
1200
+ app_code: templateDto.app_code,
1201
+ to: templateDto.to,
1202
+ message: processedMessage,
1203
+ subject: processedSubject,
1204
+ type: templateDto.type,
1205
+ templateId: templateDto.templateId,
1206
+ variables: templateDto.variables,
1207
+ };
1208
+
1209
+ return this.sendGenericMessage(messageDto);
1210
+ }
1211
+
1212
+ private detectCommunicationType(
1213
+ to: string | string[],
1214
+ type?: string,
1215
+ ): 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE' {
1216
+ if (type) {
1217
+ return type as 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
1218
+ }
1219
+
1220
+ const recipient = Array.isArray(to) ? to[0] : to;
1221
+
1222
+ // Email detection
1223
+ if (recipient.includes('@')) {
1224
+ return 'EMAIL';
1225
+ }
1226
+
1227
+ // Phone number detection (basic)
1228
+ if (/^\+?[\d\s\-\(\)]+$/.test(recipient)) {
1229
+ // Could be SMS or WhatsApp, default to SMS
1230
+ return 'SMS';
1231
+ }
1232
+
1233
+ // Default to EMAIL if unsure
1234
+ return 'EMAIL';
1235
+ }
1236
+
1237
+ private sortConfigsByPriorityAndDefault(
1238
+ configs: IntegrationConfigWithConfig[],
1239
+ _priority: string,
1240
+ ): IntegrationConfigWithConfig[] {
1241
+ return configs.sort((a, b) => {
1242
+ // First sort by default (true comes first)
1243
+ if (a.is_default !== b.is_default) {
1244
+ return b.is_default ? 1 : -1;
1245
+ }
1246
+
1247
+ // Then sort by priority (lower number = higher priority)
1248
+ if (a.priority !== b.priority) {
1249
+ return a.priority - b.priority;
1250
+ }
1251
+
1252
+ // Finally sort by created_at descending (newest first)
1253
+ return (
1254
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
1255
+ );
1256
+ });
1257
+ }
1258
+
1259
+ private async processTemplate(
1260
+ templateId: number,
1261
+ entity_type: any,
1262
+ entity_id: any,
1263
+ ): Promise<{ variables?: Record<string, any>; externalTemplateId?: string; rich_text?: string; subject?: string }> {
1264
+ const commTemplate: any = await this.entityService.getEntityData(COMM_TEMPLATE, templateId, {} as any);
1265
+ if (!commTemplate) {
1266
+ return {
1267
+ variables: {},
1268
+ externalTemplateId: undefined,
1269
+ };
1270
+ }
1271
+
1272
+ let variables = {};
1273
+ if (commTemplate.mapper_id) {
1274
+ variables = await this.fieldMapperService.resolveData(
1275
+ commTemplate.mapper_id,
1276
+ "LOOKUP",
1277
+ entity_type,
1278
+ entity_id,
1279
+ {} as any,
1280
+ );
1281
+ }
1282
+
1283
+ if (!commTemplate.template_id) {
1284
+ let richText = commTemplate.rich_text;
1285
+
1286
+ richText = richText.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1287
+ const value = variables[key];
1288
+ return value !== undefined ? String(value) : match;
1289
+ });
1290
+
1291
+ return {
1292
+ rich_text: richText,
1293
+ subject: commTemplate.subject
1294
+ };
1295
+ }
1296
+
1297
+ return {
1298
+ variables,
1299
+ externalTemplateId: commTemplate.template_id,
1300
+ };
1301
+ }
1302
+
1303
+ private replaceTemplateVariables(
1304
+ template: string,
1305
+ variables: Record<string, any>,
1306
+ ): string {
1307
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
1308
+ const value = variables[key];
1309
+ return value !== undefined ? String(value) : match;
1310
+ });
1311
+ }
1312
+
1313
+ private async getTemplate(templateId: string) {
1314
+ // Placeholder for template system integration
1315
+ // In production, this would connect to your template database/service
1316
+ return {
1317
+ id: templateId,
1318
+ subject: 'Default Subject {{variable}}',
1319
+ content: 'Hello {{name}}, this is a template message with {{variable}}.',
1320
+ };
1321
+ }
1322
+
1323
+ private chunkArray<T>(array: T[], chunkSize: number): T[][] {
1324
+ const chunks: T[][] = [];
1325
+ for (let i = 0; i < array.length; i += chunkSize) {
1326
+ chunks.push(array.slice(i, i + chunkSize));
1327
+ }
1328
+ return chunks;
1329
+ }
1330
+
1331
+ async initGmailOAuth(
1332
+ levelId: number,
1333
+ levelType: string,
1334
+ app_code: string,
1335
+ email?: string,
1336
+ ): Promise<{ authUrl: string; state: string }> {
1337
+ try {
1338
+ // Get system OAuth credentials from config
1339
+ const clientId = this.configService.get<string>('CLIENT_ID');
1340
+ const clientSecret = this.configService.get<string>('CLIENT_SECRET');
1341
+
1342
+ if (!clientId || !clientSecret) {
1343
+ throw new Error('Gmail OAuth credentials not configured');
1344
+ }
1345
+
1346
+ const state = this.generateSecureState();
1347
+ // Use the existing registered callback URL
1348
+ const callbackUrl =
1349
+ this.configService.get<string>('CALLBACK_URL') ||
1350
+ 'http://localhost:5001/auth/google/callback';
1351
+
1352
+ this.gmailOAuthStates.set(state, {
1353
+ levelId,
1354
+ levelType,
1355
+ app_code,
1356
+ email,
1357
+ timestamp: Date.now(),
1358
+ });
1359
+
1360
+ // Auto-cleanup after 10 minutes
1361
+ setTimeout(
1362
+ () => {
1363
+ this.gmailOAuthStates.delete(state);
1364
+ },
1365
+ 10 * 60 * 1000,
1366
+ );
1367
+
1368
+ const oauth2Client = new google.auth.OAuth2(
1369
+ clientId,
1370
+ clientSecret,
1371
+ callbackUrl,
1372
+ );
1373
+
1374
+ const scopes = [
1375
+ 'https://www.googleapis.com/auth/gmail.send',
1376
+ 'https://www.googleapis.com/auth/gmail.readonly',
1377
+ 'https://www.googleapis.com/auth/userinfo.email',
1378
+ 'https://www.googleapis.com/auth/calendar',
1379
+ ];
1380
+
1381
+ const authUrl = oauth2Client.generateAuthUrl({
1382
+ access_type: 'offline',
1383
+ scope: scopes,
1384
+ state: `gmail_config:${state}`, // Prefix to identify Gmail config requests
1385
+ prompt: 'consent',
1386
+ login_hint: email,
1387
+ });
1388
+
1389
+ return { authUrl, state };
1390
+ } catch (error) {
1391
+ this.logger.error('Error initializing Gmail OAuth:', error.message);
1392
+ throw new Error('Failed to initialize Gmail OAuth');
1393
+ }
1394
+ }
1395
+
1396
+ async handleGmailOAuthCallback(
1397
+ code: string,
1398
+ state: string,
1399
+ ): Promise<GmailSSOResult> {
1400
+ try {
1401
+ const oauthState = this.gmailOAuthStates.get(state);
1402
+ if (!oauthState) {
1403
+ throw new Error('Invalid or expired OAuth state');
1404
+ }
1405
+
1406
+ this.gmailOAuthStates.delete(state);
1407
+
1408
+ if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1409
+ throw new Error('OAuth state expired');
1410
+ }
1411
+
1412
+ // Get system OAuth credentials
1413
+ const clientId = this.configService.get<string>('CLIENT_ID');
1414
+ const clientSecret = this.configService.get<string>('CLIENT_SECRET');
1415
+ const callbackUrl =
1416
+ this.configService.get<string>('CALLBACK_URL') ||
1417
+ 'http://localhost:5001/auth/google/callback';
1418
+
1419
+ const oauth2Client = new google.auth.OAuth2(
1420
+ clientId,
1421
+ clientSecret,
1422
+ callbackUrl,
1423
+ );
1424
+
1425
+ const { tokens } = await oauth2Client.getToken(code);
1426
+
1427
+ if (!tokens.access_token) {
1428
+ throw new Error('Failed to obtain access token');
1429
+ }
1430
+
1431
+ // Get user email from Google
1432
+ oauth2Client.setCredentials(tokens);
1433
+ const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
1434
+ const userInfo = await oauth2.userinfo.get();
1435
+ const email = userInfo.data.email;
1436
+
1437
+ if (!email) {
1438
+ throw new Error('Failed to get user email');
1439
+ }
1440
+
1441
+ // Verify email matches if hint was provided
1442
+ if (oauthState.email && oauthState.email !== email) {
1443
+ throw new Error('Email mismatch with OAuth hint');
1444
+ }
1445
+
1446
+ const gmailConfig = {
1447
+ clientId,
1448
+ clientSecret,
1449
+ email: email,
1450
+ accessToken: tokens.access_token,
1451
+ refreshToken: tokens.refresh_token,
1452
+ scope: tokens.scope,
1453
+ tokenType: tokens.token_type,
1454
+ expiryDate: tokens.expiry_date,
1455
+ };
1456
+
1457
+ // Validate that no active EMAIL configuration exists
1458
+ await this.validateUniqueActiveConfig(
1459
+ oauthState.levelId,
1460
+ oauthState.levelType,
1461
+ oauthState.app_code,
1462
+ 'EMAIL',
1463
+ 'gmail',
1464
+ );
1465
+
1466
+ // Create integration config
1467
+ const config = this.configRepository.create({
1468
+ app_code: oauthState.app_code,
1469
+ integration_type: 'EMAIL',
1470
+ integration_provider: 'gmail',
1471
+ integration_source_id: 1, // Gmail source ID
1472
+ level_id: oauthState.levelId,
1473
+ level_type: oauthState.levelType,
1474
+ status: 1,
1475
+ priority: 1,
1476
+ is_default: false,
1477
+ config_json: gmailConfig as any,
1478
+ });
1479
+
1480
+ const savedConfig = await this.configRepository.save(config);
1481
+
1482
+ this.logger.log(
1483
+ `Gmail OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1484
+ );
1485
+
1486
+ return {
1487
+ hubId: savedConfig.id,
1488
+ configId: savedConfig.id,
1489
+ };
1490
+ } catch (error) {
1491
+ this.logger.error('Error handling Gmail OAuth callback:', error.message);
1492
+ throw new Error(`Failed to complete Gmail OAuth: ${error.message}`);
1493
+ }
1494
+ }
1495
+
1496
+ async handleGmailTokensCallback(
1497
+ email: string,
1498
+ accessToken: string,
1499
+ refreshToken: string,
1500
+ state: string,
1501
+ ): Promise<GmailSSOResult> {
1502
+ try {
1503
+ const oauthState = this.gmailOAuthStates.get(state);
1504
+ if (!oauthState) {
1505
+ throw new Error('Invalid or expired OAuth state');
1506
+ }
1507
+
1508
+ this.gmailOAuthStates.delete(state);
1509
+
1510
+ if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1511
+ throw new Error('OAuth state expired');
1512
+ }
1513
+
1514
+ // Verify email matches if hint was provided
1515
+ if (oauthState.email && oauthState.email !== email) {
1516
+ throw new Error('Email mismatch with OAuth hint');
1517
+ }
1518
+
1519
+ // Validate that no active EMAIL configuration exists
1520
+ await this.validateUniqueActiveConfig(
1521
+ oauthState.levelId,
1522
+ oauthState.levelType,
1523
+ oauthState.app_code,
1524
+ 'EMAIL',
1525
+ 'gmail',
1526
+ );
1527
+
1528
+ // Pure SSO configuration - no client credentials stored per user
1529
+ const gmailConfig = {
1530
+ email: email,
1531
+ accessToken: accessToken,
1532
+ refreshToken: refreshToken,
1533
+ authMethod: 'GOOGLE_SSO',
1534
+ scopes: [
1535
+ 'https://www.googleapis.com/auth/gmail.send',
1536
+ 'https://www.googleapis.com/auth/userinfo.email',
1537
+ ],
1538
+ };
1539
+
1540
+ // Create integration config
1541
+ const config = this.configRepository.create({
1542
+ app_code: oauthState.app_code,
1543
+ integration_type: 'EMAIL',
1544
+ integration_provider: 'gmail',
1545
+ integration_source_id: 1, // Gmail source ID
1546
+ level_id: oauthState.levelId,
1547
+ level_type: oauthState.levelType,
1548
+ status: 1,
1549
+ priority: 1,
1550
+ is_default: false,
1551
+ config_json: gmailConfig as any,
1552
+ });
1553
+
1554
+ const savedConfig = await this.configRepository.save(config);
1555
+
1556
+ this.logger.log(
1557
+ `Gmail tokens configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1558
+ );
1559
+
1560
+ return {
1561
+ hubId: savedConfig.id,
1562
+ configId: savedConfig.id,
1563
+ };
1564
+ } catch (error) {
1565
+ this.logger.error('Error handling Gmail tokens callback:', error.message);
1566
+ throw new Error(
1567
+ `Failed to complete Gmail tokens callback: ${error.message}`,
1568
+ );
1569
+ }
1570
+ }
1571
+
1572
+ async testGmailConfig(
1573
+ hubId: number,
1574
+ ): Promise<{ success: boolean; error?: string }> {
1575
+ try {
1576
+ const integrationConfig = await this.configRepository.findOne({
1577
+ where: { id: hubId },
1578
+ });
1579
+
1580
+ if (!integrationConfig) {
1581
+ throw new Error('Integration config not found');
1582
+ }
1583
+
1584
+ const isValid = await this.gmailApiStrategy.validateConnection(
1585
+ integrationConfig.config_json,
1586
+ );
1587
+
1588
+ if (!isValid) {
1589
+ return {
1590
+ success: false,
1591
+ error: 'Gmail configuration is invalid or expired',
1592
+ };
1593
+ }
1594
+
1595
+ return { success: true };
1596
+ } catch (error) {
1597
+ this.logger.error('Error testing Gmail config:', error.message);
1598
+ return { success: false, error: error.message };
1599
+ }
1600
+ }
1601
+
1602
+ private generateSecureState(): string {
1603
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
1604
+ }
1605
+
1606
+ private async validateUniqueActiveConfig(
1607
+ levelId: number,
1608
+ levelType: string,
1609
+ app_code: string,
1610
+ configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1611
+ provider: string,
1612
+ excludeHubId?: number,
1613
+ ): Promise<void> {
1614
+ // Find all active configurations of the same provider for this level and app_code
1615
+ const query = this.configRepository
1616
+ .createQueryBuilder('hub')
1617
+ .where('hub.level_id = :levelId', { levelId })
1618
+ .andWhere('hub.level_type = :levelType', { levelType })
1619
+ .andWhere('hub.app_code = :app_code', { app_code })
1620
+ .andWhere('hub.integration_type = :configType', { configType })
1621
+ .andWhere('hub.integration_provider = :provider', { provider })
1622
+ .andWhere('hub.status = :status', { status: 1 });
1623
+
1624
+ // Exclude current hub if updating
1625
+ if (excludeHubId) {
1626
+ query.andWhere('hub.id != :excludeHubId', { excludeHubId });
1627
+ }
1628
+
1629
+ const existingActiveConfigs = await query.getMany();
1630
+
1631
+ // If there are existing active configurations of the same provider, throw error
1632
+ if (existingActiveConfigs.length > 0) {
1633
+ throw new Error(
1634
+ `A ${provider} configuration already exists for ${configType} in app_code ${app_code}, ${levelType} ${levelId}. Only one configuration per provider is allowed.`,
1635
+ );
1636
+ }
1637
+ }
1638
+
1639
+ private requiresOAuthFlow(
1640
+ configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1641
+ provider: string,
1642
+ config: any,
1643
+ ): boolean {
1644
+ // Check if config indicates OAuth is needed (missing tokens or explicit OAuth request)
1645
+ const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
1646
+
1647
+ switch (key) {
1648
+ case 'email_gmail':
1649
+ // Requires OAuth if no access token provided or OAuth explicitly requested
1650
+ return !config.accessToken || config.useOAuth === true;
1651
+
1652
+ case 'email_outlook':
1653
+ // Requires OAuth if no access token provided or OAuth explicitly requested
1654
+ return !config.accessToken || config.useOAuth === true;
1655
+
1656
+ default:
1657
+ // Most other providers don't require OAuth
1658
+ return false;
1659
+ }
1660
+ }
1661
+
1662
+ private async generateOAuthUrl(
1663
+ levelId: number,
1664
+ levelType: string,
1665
+ app_code: string,
1666
+ configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1667
+ provider: string,
1668
+ integration_source_id: number,
1669
+ config: any,
1670
+ priority?: number,
1671
+ is_default?: boolean,
1672
+ ): Promise<{ authUrl: string; state: string; message: string }> {
1673
+ const key = `${configType.toLowerCase()}_${provider.toLowerCase()}`;
1674
+
1675
+ switch (key) {
1676
+ case 'email_gmail':
1677
+ const gmailResult = await this.initGmailOAuth(
1678
+ levelId,
1679
+ levelType,
1680
+ app_code,
1681
+ config.email,
1682
+ );
1683
+ return {
1684
+ authUrl: gmailResult.authUrl,
1685
+ state: gmailResult.state,
1686
+ message:
1687
+ 'Please complete Gmail OAuth authorization. Configuration will be created automatically after authorization.',
1688
+ };
1689
+
1690
+ case 'email_outlook':
1691
+ const outlookResult = await this.initOutlookOAuth(
1692
+ levelId,
1693
+ levelType,
1694
+ app_code,
1695
+ config.email,
1696
+ );
1697
+ return {
1698
+ authUrl: outlookResult.authUrl,
1699
+ state: outlookResult.state,
1700
+ message:
1701
+ 'Please complete Outlook OAuth authorization. Configuration will be created automatically after authorization.',
1702
+ };
1703
+
1704
+ default:
1705
+ throw new Error(`OAuth not supported for ${configType}/${provider}`);
1706
+ }
1707
+ }
1708
+
1709
+ private async createDirectConfig(
1710
+ levelId: number,
1711
+ levelType: string,
1712
+ app_code: string,
1713
+ configType: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
1714
+ provider: string,
1715
+ integration_source_id: number,
1716
+ config: any,
1717
+ priority?: number,
1718
+ is_default?: boolean,
1719
+ ): Promise<IntegrationConfig> {
1720
+ // Validate that no duplicate provider configurations exist
1721
+ await this.validateUniqueActiveConfig(
1722
+ levelId,
1723
+ levelType,
1724
+ app_code,
1725
+ configType,
1726
+ provider,
1727
+ );
1728
+
1729
+ // Deactivate all other configurations of the same integration type
1730
+ await this.configRepository.update(
1731
+ {
1732
+ level_id: levelId,
1733
+ level_type: levelType,
1734
+ app_code: app_code,
1735
+ integration_type: configType,
1736
+ status: 1,
1737
+ },
1738
+ { status: 0 },
1739
+ );
1740
+
1741
+ // If setting as default, remove default from other configurations of same type and app_code
1742
+ if (is_default) {
1743
+ await this.configRepository.update(
1744
+ {
1745
+ level_id: levelId,
1746
+ level_type: levelType,
1747
+ app_code: app_code,
1748
+ integration_type: configType,
1749
+ },
1750
+ { is_default: false },
1751
+ );
1752
+ }
1753
+
1754
+ // Create integration config
1755
+ const integrationConfig = this.configRepository.create({
1756
+ app_code: app_code,
1757
+ integration_type: configType,
1758
+ integration_provider: provider,
1759
+ integration_source_id: integration_source_id,
1760
+ level_id: levelId,
1761
+ level_type: levelType,
1762
+ status: 1,
1763
+ priority: priority || 1,
1764
+ is_default: is_default || false,
1765
+ config_json: config,
1766
+ });
1767
+
1768
+ const savedConfig = await this.configRepository.save(integrationConfig);
1769
+ this.logger.log(
1770
+ `Communication config created: ${configType}/${provider} for ${levelType} ${levelId}`,
1771
+ );
1772
+
1773
+ // Store DID mapping for TELEPHONE integration
1774
+ if (configType === 'TELEPHONE') {
1775
+ try {
1776
+ // Extract DID from config (could be did, callerNumber, or fromNumber)
1777
+ const did = config.did || config.callerNumber || config.fromNumber;
1778
+
1779
+ if (did) {
1780
+ const entityMapper = this.entityMapperRepository.create({
1781
+ integration_config_id: savedConfig.id,
1782
+ level_id: String(levelId),
1783
+ level_type: levelType,
1784
+ appcode: app_code,
1785
+ did: did,
1786
+ campaign_name: config.campaignName || null,
1787
+ });
1788
+
1789
+ await this.entityMapperRepository.save(entityMapper);
1790
+ this.logger.log(
1791
+ `DID mapping created for TELEPHONE integration: ${did} for ${levelType} ${levelId}`,
1792
+ );
1793
+ }
1794
+ } catch (error) {
1795
+ this.logger.warn(
1796
+ `Failed to create DID mapping for TELEPHONE integration: ${error.message}`,
1797
+ );
1798
+ }
1799
+ }
1800
+
1801
+ return Array.isArray(savedConfig) ? savedConfig[0] : savedConfig;
1802
+ }
1803
+
1804
+ async initOutlookOAuth(
1805
+ levelId: number,
1806
+ levelType: string,
1807
+ app_code: string,
1808
+ email?: string,
1809
+ ): Promise<{ authUrl: string; state: string }> {
1810
+ try {
1811
+ // Get system OAuth credentials from config
1812
+ const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
1813
+ const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
1814
+
1815
+ if (!clientId || !tenantId) {
1816
+ throw new Error('Outlook OAuth credentials not configured');
1817
+ }
1818
+
1819
+ const state = this.generateSecureState();
1820
+ const callbackUrl =
1821
+ this.configService.get<string>('OUTLOOK_CALLBACK_URL') ||
1822
+ 'http://localhost:5001/auth/outlook/callback';
1823
+
1824
+ this.gmailOAuthStates.set(state, {
1825
+ levelId,
1826
+ levelType,
1827
+ app_code,
1828
+ email,
1829
+ timestamp: Date.now(),
1830
+ });
1831
+
1832
+ // Auto-cleanup after 10 minutes
1833
+ setTimeout(
1834
+ () => {
1835
+ this.gmailOAuthStates.delete(state);
1836
+ },
1837
+ 10 * 60 * 1000,
1838
+ );
1839
+
1840
+ const scopes = [
1841
+ 'https://graph.microsoft.com/Mail.Send',
1842
+ 'https://graph.microsoft.com/User.Read',
1843
+ ];
1844
+
1845
+ const authUrl =
1846
+ `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` +
1847
+ `client_id=${clientId}&` +
1848
+ `response_type=code&` +
1849
+ `redirect_uri=${encodeURIComponent(callbackUrl)}&` +
1850
+ `scope=${encodeURIComponent(scopes.join(' '))}&` +
1851
+ `state=outlook_config:${state}&` +
1852
+ `response_mode=query&` +
1853
+ `prompt=consent` +
1854
+ (email ? `&login_hint=${encodeURIComponent(email)}` : '');
1855
+
1856
+ return { authUrl, state };
1857
+ } catch (error) {
1858
+ this.logger.error('Error initializing Outlook OAuth:', error.message);
1859
+ throw new Error('Failed to initialize Outlook OAuth');
1860
+ }
1861
+ }
1862
+
1863
+ async handleOutlookOAuthCallback(
1864
+ code: string,
1865
+ state: string,
1866
+ ): Promise<GmailSSOResult> {
1867
+ try {
1868
+ const oauthState = this.gmailOAuthStates.get(state);
1869
+ if (!oauthState) {
1870
+ throw new Error('Invalid or expired OAuth state');
1871
+ }
1872
+
1873
+ this.gmailOAuthStates.delete(state);
1874
+
1875
+ if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
1876
+ throw new Error('OAuth state expired');
1877
+ }
1878
+
1879
+ const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
1880
+ const clientSecret = this.configService.get<string>(
1881
+ 'OUTLOOK_CLIENT_SECRET',
1882
+ );
1883
+ const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
1884
+ const callbackUrl =
1885
+ this.configService.get<string>('OUTLOOK_CALLBACK_URL') ||
1886
+ 'http://localhost:5001/auth/outlook/callback';
1887
+
1888
+ // Exchange code for tokens
1889
+ const tokenResponse = await fetch(
1890
+ `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
1891
+ {
1892
+ method: 'POST',
1893
+ headers: {
1894
+ 'Content-Type': 'application/x-www-form-urlencoded',
1895
+ },
1896
+ body: new URLSearchParams({
1897
+ client_id: clientId!,
1898
+ client_secret: clientSecret!,
1899
+ code: code,
1900
+ redirect_uri: callbackUrl,
1901
+ grant_type: 'authorization_code',
1902
+ }),
1903
+ },
1904
+ );
1905
+
1906
+ const tokens = await tokenResponse.json();
1907
+
1908
+ if (!tokens.access_token) {
1909
+ throw new Error('Failed to obtain access token');
1910
+ }
1911
+
1912
+ // Get user info from Microsoft Graph
1913
+ const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
1914
+ headers: {
1915
+ Authorization: `Bearer ${tokens.access_token}`,
1916
+ },
1917
+ });
1918
+
1919
+ const userInfo = await userResponse.json();
1920
+ const email = userInfo.mail || userInfo.userPrincipalName;
1921
+
1922
+ if (!email) {
1923
+ throw new Error('Failed to get user email');
1924
+ }
1925
+
1926
+ // Validate that no active EMAIL configuration exists
1927
+ await this.validateUniqueActiveConfig(
1928
+ oauthState.levelId,
1929
+ oauthState.levelType,
1930
+ oauthState.app_code,
1931
+ 'EMAIL',
1932
+ 'outlook',
1933
+ );
1934
+
1935
+ const outlookConfig = {
1936
+ clientId,
1937
+ tenantId,
1938
+ email: email,
1939
+ accessToken: tokens.access_token,
1940
+ refreshToken: tokens.refresh_token,
1941
+ scope: tokens.scope,
1942
+ tokenType: tokens.token_type,
1943
+ expiresIn: tokens.expires_in,
1944
+ };
1945
+
1946
+ // Create integration config
1947
+ const config = this.configRepository.create({
1948
+ app_code: oauthState.app_code,
1949
+ integration_type: 'EMAIL',
1950
+ integration_provider: 'outlook',
1951
+ integration_source_id: 1, // Outlook source ID
1952
+ level_id: oauthState.levelId,
1953
+ level_type: oauthState.levelType,
1954
+ status: 1,
1955
+ priority: 1,
1956
+ is_default: false,
1957
+ config_json: outlookConfig as any,
1958
+ });
1959
+
1960
+ const savedConfig = await this.configRepository.save(config);
1961
+
1962
+ this.logger.log(
1963
+ `Outlook OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
1964
+ );
1965
+
1966
+ return {
1967
+ hubId: savedConfig.id,
1968
+ configId: savedConfig.id,
1969
+ };
1970
+ } catch (error) {
1971
+ this.logger.error(
1972
+ 'Error handling Outlook OAuth callback:',
1973
+ error.message,
1974
+ );
1975
+ throw new Error(`Failed to complete Outlook OAuth: ${error.message}`);
1976
+ }
1977
+ }
1978
+
1979
+ async getIntegrationConfigById(hubId: number): Promise<
1980
+ | (IntegrationConfig & {
1981
+ config: IntegrationConfig;
1982
+ linkedSource?: string;
1983
+ configDetails?: any;
1984
+ })
1985
+ | null
1986
+ > {
1987
+ try {
1988
+ // Find the integration config by ID
1989
+ const integrationConfig = await this.configRepository.findOne({
1990
+ where: { id: hubId },
1991
+ });
1992
+
1993
+ if (!integrationConfig) {
1994
+ return null;
1995
+ }
1996
+
1997
+ // Extract linked source and config details
1998
+ const linkedSource = this.extractLinkedSource(
1999
+ integrationConfig.integration_type,
2000
+ integrationConfig.integration_provider,
2001
+ integrationConfig.config_json,
2002
+ );
2003
+
2004
+ const configDetails = this.extractConfigDetails(
2005
+ integrationConfig.integration_type,
2006
+ integrationConfig.integration_provider,
2007
+ integrationConfig.config_json,
2008
+ );
2009
+
2010
+ return {
2011
+ ...integrationConfig,
2012
+ config: integrationConfig,
2013
+ linkedSource,
2014
+ configDetails,
2015
+ };
2016
+ } catch (error) {
2017
+ this.logger.error(
2018
+ `Error fetching communication config by ID ${hubId}:`,
2019
+ error.message,
2020
+ );
2021
+ throw new Error(
2022
+ `Failed to fetch communication configuration: ${error.message}`,
2023
+ );
2024
+ }
2025
+ }
2026
+
2027
+ async getIntegrationTemplates(
2028
+ levelId: number,
2029
+ levelType: string,
2030
+ app_code: string,
2031
+ integration_type: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE',
2032
+ ): Promise<{
2033
+ success: boolean;
2034
+ data?: Array<{
2035
+ label: string;
2036
+ value: string;
2037
+ }>;
2038
+ error?: string;
2039
+ }> {
2040
+ try {
2041
+ // Get active configuration for the integration type
2042
+ const config = await this.getSingleActiveConfig(
2043
+ levelId,
2044
+ levelType,
2045
+ app_code,
2046
+ integration_type,
2047
+ );
2048
+
2049
+ if (!config) {
2050
+ return {
2051
+ success: false,
2052
+ error: `No active ${integration_type} configuration found for this level`,
2053
+ };
2054
+ }
2055
+
2056
+ // Create strategy instance
2057
+ const strategy = this.integrationFactory.create(
2058
+ config.integration_type,
2059
+ 'API',
2060
+ config.integration_provider,
2061
+ );
2062
+
2063
+ // Check if strategy supports getTemplates
2064
+ if (typeof (strategy as any).getTemplates !== 'function') {
2065
+ return {
2066
+ success: false,
2067
+ error: `Template retrieval not supported for provider: ${config.integration_provider}`,
2068
+ };
2069
+ }
2070
+
2071
+ // Call getTemplates method
2072
+ return await (strategy as any).getTemplates(config.config_json);
2073
+ } catch (error) {
2074
+ this.logger.error(
2075
+ `Error fetching templates for ${integration_type} level ${levelId}/${levelType}:`,
2076
+ error.message,
2077
+ );
2078
+ return {
2079
+ success: false,
2080
+ error: `Failed to fetch templates: ${error.message}`,
2081
+ };
2082
+ }
2083
+ }
2084
+
2085
+ async getSendGridTemplates(
2086
+ levelId: number,
2087
+ levelType: string,
2088
+ app_code: string,
2089
+ ): Promise<{
2090
+ success: boolean;
2091
+ data?: Array<{
2092
+ label: string;
2093
+ value: string;
2094
+ }>;
2095
+ error?: string;
2096
+ }> {
2097
+ try {
2098
+ // Find active SendGrid configurations for this level
2099
+ const hubs = await this.getActiveConfigs(
2100
+ levelId,
2101
+ levelType,
2102
+ app_code,
2103
+ 'EMAIL',
2104
+ );
2105
+
2106
+ // Look for SendGrid provider
2107
+ const sendGridHub = hubs.find(
2108
+ (hub) => hub.integration_provider === 'sendgrid',
2109
+ );
2110
+
2111
+ if (!sendGridHub) {
2112
+ return {
2113
+ success: false,
2114
+ error: 'No active SendGrid configuration found for this level',
2115
+ };
2116
+ }
2117
+
2118
+ // Extract API key from configuration
2119
+ const apiKey = sendGridHub.config_json?.apiKey;
2120
+
2121
+ if (!apiKey) {
2122
+ return {
2123
+ success: false,
2124
+ error: 'SendGrid API key not found in configuration',
2125
+ };
2126
+ }
2127
+
2128
+ // Use the SendGrid strategy to fetch templates
2129
+ return this.sendGridApiStrategy.getTemplates(apiKey);
2130
+ } catch (error) {
2131
+ this.logger.error(
2132
+ `Error fetching SendGrid templates for level ${levelId}/${levelType}:`,
2133
+ error.message,
2134
+ );
2135
+ return {
2136
+ success: false,
2137
+ error: `Failed to fetch SendGrid templates: ${error.message}`,
2138
+ };
2139
+ }
2140
+ }
2141
+
2142
+ async getSendGridVerifiedSenders(apiKey: string): Promise<{
2143
+ success: boolean;
2144
+ data?: Array<{
2145
+ label: string;
2146
+ value: string;
2147
+ }>;
2148
+ error?: string;
2149
+ }> {
2150
+ try {
2151
+ if (!apiKey) {
2152
+ return {
2153
+ success: false,
2154
+ error: 'SendGrid API key not found in configuration',
2155
+ };
2156
+ }
2157
+
2158
+ // Use the SendGrid strategy to fetch verified senders
2159
+ return this.sendGridApiStrategy.getVerifiedSenders(apiKey);
2160
+ } catch (error) {
2161
+ this.logger.error(
2162
+ `Error fetching SendGrid verified senders`,
2163
+ error.message,
2164
+ );
2165
+ return {
2166
+ success: false,
2167
+ error: `Failed to fetch SendGrid verified senders: ${error.message}`,
2168
+ };
2169
+ }
2170
+ }
2171
+
2172
+ // UserIntegration Management Methods
2173
+
2174
+ async createUserIntegration(
2175
+ createDto: CreateUserIntegrationDto,
2176
+ ): Promise<UserIntegration> {
2177
+ try {
2178
+ // Check if mapping already exists
2179
+ const existing = await this.userIntegrationRepository.findOne({
2180
+ where: {
2181
+ user_id: createDto.user_id,
2182
+ integration_config_id: createDto.integration_config_id,
2183
+ },
2184
+ });
2185
+
2186
+ if (existing) {
2187
+ throw new Error('User integration mapping already exists');
2188
+ }
2189
+
2190
+ // Verify integration config exists
2191
+ const config = await this.configRepository.findOne({
2192
+ where: { id: createDto.integration_config_id },
2193
+ });
2194
+
2195
+ if (!config) {
2196
+ throw new Error('Integration configuration not found');
2197
+ }
2198
+
2199
+ const userIntegration = this.userIntegrationRepository.create(createDto);
2200
+ return await this.userIntegrationRepository.save(userIntegration);
2201
+ } catch (error) {
2202
+ this.logger.error(
2203
+ `Error creating user integration mapping: ${error.message}`,
2204
+ );
2205
+ throw error;
2206
+ }
2207
+ }
2208
+
2209
+ async bulkCreateUserIntegration(
2210
+ bulkDto: BulkCreateUserIntegrationDto,
2211
+ ): Promise<{
2212
+ created: UserIntegration[];
2213
+ updated: UserIntegration[];
2214
+ failed: Array<{ data: CreateUserIntegrationDto; error: string }>;
2215
+ summary: {
2216
+ total: number;
2217
+ created: number;
2218
+ updated: number;
2219
+ failed: number;
2220
+ };
2221
+ }> {
2222
+ const created: UserIntegration[] = [];
2223
+ const updated: UserIntegration[] = [];
2224
+ const failed: Array<{ data: CreateUserIntegrationDto; error: string }> = [];
2225
+
2226
+ try {
2227
+ for (const createDto of bulkDto.user_integrations) {
2228
+ try {
2229
+ // Check if mapping already exists
2230
+ const existing = await this.userIntegrationRepository.findOne({
2231
+ where: {
2232
+ user_id: createDto.user_id,
2233
+ integration_config_id: createDto.integration_config_id,
2234
+ },
2235
+ });
2236
+
2237
+ if (existing) {
2238
+ // Update existing mapping
2239
+ if (createDto.external_user_id !== undefined) {
2240
+ existing.external_user_id = createDto.external_user_id;
2241
+ }
2242
+ if (createDto.external_user_data !== undefined) {
2243
+ existing.external_user_data = createDto.external_user_data;
2244
+ }
2245
+ if (createDto.is_active !== undefined) {
2246
+ existing.is_active = createDto.is_active;
2247
+ }
2248
+
2249
+ const updatedIntegration =
2250
+ await this.userIntegrationRepository.save(existing);
2251
+ updated.push(updatedIntegration);
2252
+
2253
+ this.logger.log(
2254
+ `Updated user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`,
2255
+ );
2256
+ } else {
2257
+ // Verify integration config exists
2258
+ const config = await this.configRepository.findOne({
2259
+ where: { id: createDto.integration_config_id },
2260
+ });
2261
+
2262
+ if (!config) {
2263
+ failed.push({
2264
+ data: createDto,
2265
+ error: 'Integration configuration not found',
2266
+ });
2267
+ continue;
2268
+ }
2269
+
2270
+ // Create new mapping
2271
+ const userIntegration =
2272
+ this.userIntegrationRepository.create(createDto);
2273
+ const savedIntegration =
2274
+ await this.userIntegrationRepository.save(userIntegration);
2275
+ created.push(savedIntegration);
2276
+
2277
+ this.logger.log(
2278
+ `Created user integration mapping for user ${createDto.user_id} and config ${createDto.integration_config_id}`,
2279
+ );
2280
+ }
2281
+ } catch (error) {
2282
+ failed.push({
2283
+ data: createDto,
2284
+ error: error.message || 'Unknown error',
2285
+ });
2286
+ this.logger.error(
2287
+ `Error processing user integration for user ${createDto.user_id}: ${error.message}`,
2288
+ );
2289
+ }
2290
+ }
2291
+
2292
+ const summary = {
2293
+ total: bulkDto.user_integrations.length,
2294
+ created: created.length,
2295
+ updated: updated.length,
2296
+ failed: failed.length,
2297
+ };
2298
+
2299
+ this.logger.log(
2300
+ `Bulk user integration completed: ${summary.created} created, ${summary.updated} updated, ${summary.failed} failed`,
2301
+ );
2302
+
2303
+ return { created, updated, failed, summary };
2304
+ } catch (error) {
2305
+ this.logger.error(
2306
+ `Error in bulk user integration creation: ${error.message}`,
2307
+ );
2308
+ throw error;
2309
+ }
2310
+ }
2311
+
2312
+ async getUserIntegrations(userId: number): Promise<UserIntegration[]> {
2313
+ try {
2314
+ return await this.userIntegrationRepository.find({
2315
+ where: { user_id: userId, is_active: true },
2316
+ order: { created_at: 'DESC' },
2317
+ });
2318
+ } catch (error) {
2319
+ this.logger.error(
2320
+ `Error fetching user integrations for user ${userId}: ${error.message}`,
2321
+ );
2322
+ throw error;
2323
+ }
2324
+ }
2325
+
2326
+ async getConfigUserIntegrations(
2327
+ configId: number,
2328
+ ): Promise<UserIntegration[]> {
2329
+ try {
2330
+ return await this.userIntegrationRepository.find({
2331
+ where: { integration_config_id: configId, is_active: true },
2332
+ order: { created_at: 'DESC' },
2333
+ });
2334
+ } catch (error) {
2335
+ this.logger.error(
2336
+ `Error fetching user integrations for config ${configId}: ${error.message}`,
2337
+ );
2338
+ throw error;
2339
+ }
2340
+ }
2341
+
2342
+ async getUserIntegrationByUserAndConfig(
2343
+ userId: number,
2344
+ configId: number,
2345
+ ): Promise<UserIntegration | null> {
2346
+ try {
2347
+ return await this.userIntegrationRepository.findOne({
2348
+ where: {
2349
+ user_id: userId,
2350
+ integration_config_id: configId,
2351
+ is_active: true,
2352
+ },
2353
+ });
2354
+ } catch (error) {
2355
+ this.logger.error(
2356
+ `Error fetching user integration for user ${userId} and config ${configId}: ${error.message}`,
2357
+ );
2358
+ throw error;
2359
+ }
2360
+ }
2361
+
2362
+ async updateUserIntegration(
2363
+ id: number,
2364
+ updateDto: UpdateUserIntegrationDto,
2365
+ ): Promise<UserIntegration> {
2366
+ try {
2367
+ const userIntegration = await this.userIntegrationRepository.findOne({
2368
+ where: { id },
2369
+ });
2370
+
2371
+ if (!userIntegration) {
2372
+ throw new Error('User integration mapping not found');
2373
+ }
2374
+
2375
+ Object.assign(userIntegration, updateDto);
2376
+ return await this.userIntegrationRepository.save(userIntegration);
2377
+ } catch (error) {
2378
+ this.logger.error(
2379
+ `Error updating user integration ${id}: ${error.message}`,
2380
+ );
2381
+ throw error;
2382
+ }
2383
+ }
2384
+
2385
+ async deleteUserIntegration(id: number): Promise<void> {
2386
+ try {
2387
+ const userIntegration = await this.userIntegrationRepository.findOne({
2388
+ where: { id },
2389
+ });
2390
+
2391
+ if (!userIntegration) {
2392
+ throw new Error('User integration mapping not found');
2393
+ }
2394
+
2395
+ await this.userIntegrationRepository.remove(userIntegration);
2396
+ } catch (error) {
2397
+ this.logger.error(
2398
+ `Error deleting user integration ${id}: ${error.message}`,
2399
+ );
2400
+ throw error;
2401
+ }
2402
+ }
2403
+
2404
+ /**
2405
+ * Get user integration data for a specific integration strategy
2406
+ * This method is intended to be used by strategies that need user mapping
2407
+ */
2408
+ async getUserIntegrationForStrategy(
2409
+ userId: number,
2410
+ integrationConfigId: number,
2411
+ ): Promise<any | null> {
2412
+ try {
2413
+ const userIntegration = await this.getUserIntegrationByUserAndConfig(
2414
+ userId,
2415
+ integrationConfigId,
2416
+ );
2417
+
2418
+ if (!userIntegration) {
2419
+ return null;
2420
+ }
2421
+
2422
+ return {
2423
+ external_user_id: userIntegration.external_user_id,
2424
+ external_user_data: userIntegration.external_user_data,
2425
+ };
2426
+ } catch (error) {
2427
+ this.logger.error(
2428
+ `Error fetching user integration data for strategy: ${error.message}`,
2429
+ );
2430
+ return null;
2431
+ }
2432
+ }
2433
+
2434
+ /**
2435
+ * Check agent status for TELEPHONE integration
2436
+ * Works with any provider that implements checkAgentStatus method
2437
+ */
2438
+ async checkAgentStatus(
2439
+ levelId: number,
2440
+ levelType: string,
2441
+ appCode: string,
2442
+ userId: number,
2443
+ integrationType: 'TELEPHONE' = 'TELEPHONE',
2444
+ ): Promise<{
2445
+ success: boolean;
2446
+ isReady?: boolean;
2447
+ state?: string;
2448
+ agentInfo?: any;
2449
+ error?: string;
2450
+ }> {
2451
+ try {
2452
+ // Get the active configuration
2453
+ const config = await this.getSingleActiveConfig(
2454
+ levelId,
2455
+ levelType,
2456
+ appCode,
2457
+ integrationType,
2458
+ );
2459
+
2460
+ if (!config) {
2461
+ return {
2462
+ success: false,
2463
+ error: 'No active TELEPHONE configuration found',
2464
+ };
2465
+ }
2466
+
2467
+ // Get user integration mapping
2468
+ const userIntegration = await this.getUserIntegrationByUserAndConfig(
2469
+ userId,
2470
+ config.id,
2471
+ );
2472
+
2473
+ if (!userIntegration) {
2474
+ return {
2475
+ success: false,
2476
+ error: 'User integration mapping not found. Please configure agent details.',
2477
+ };
2478
+ }
2479
+
2480
+ // Create strategy instance
2481
+ const strategy = this.integrationFactory.create(
2482
+ config.integration_type,
2483
+ 'API', // service is deprecated, using default
2484
+ config.integration_provider,
2485
+ );
2486
+
2487
+ // Get the merged config with user data
2488
+ let mergedConfig = config.config_json;
2489
+
2490
+ if (strategy.createUserSpecificConfig) {
2491
+ mergedConfig = strategy.createUserSpecificConfig(
2492
+ config.config_json,
2493
+ userIntegration.external_user_id,
2494
+ );
2495
+ }
2496
+
2497
+ // Check if strategy supports agent status check
2498
+ if (typeof (strategy as any).checkAgentStatus !== 'function') {
2499
+ return {
2500
+ success: false,
2501
+ error: `Agent status check not supported for provider: ${config.integration_provider}`,
2502
+ };
2503
+ }
2504
+
2505
+ // Check agent status
2506
+ const statusResult = await (strategy as any).checkAgentStatus(
2507
+ mergedConfig,
2508
+ );
2509
+
2510
+ return {
2511
+ success: true,
2512
+ isReady: statusResult.isReady,
2513
+ state: statusResult.state,
2514
+ error: statusResult.error,
2515
+ };
2516
+ } catch (error) {
2517
+ this.logger.error(
2518
+ `Error checking agent status: ${error.message}`,
2519
+ error.stack,
2520
+ );
2521
+ return {
2522
+ success: false,
2523
+ error: error.message || 'Failed to check agent status',
2524
+ };
2525
+ }
2526
+ }
2527
+ }