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