diff --git a/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.js b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.js index f9b45d9d223..3f57b6a9be4 100644 --- a/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.js +++ b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.js @@ -12,6 +12,7 @@ const { CALL_DATA_NOT_PROVIDED_ERROR, INVALID_CALL_META_ERROR, CALL_STATUS_STARTED, + NATIVE_VOIP_TYPE, } = require('@condo/domains/miniapp/constants') const { RejectCallError, @@ -24,9 +25,10 @@ const { parseSendMessageResults, sendMessageToUser, COMMON_VOIP_ERRORS, + getVoipTypeByData, } = require('@condo/domains/miniapp/utils/sendVoIPCallMessage') const { setCallStatus, generateCallStatusToken, isCallIdValid, MAX_CALL_META_LENGTH, isCallMetaValid, buildCallStatusJWTToken } = require('@condo/domains/miniapp/utils/voip') -const { VOIP_INCOMING_CALL_MESSAGE_TYPE } = require('@condo/domains/notification/constants/constants') +const { VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE } = require('@condo/domains/notification/constants/constants') const { UNIT_TYPES } = require('@condo/domains/property/constants/common') const { RedisGuard } = require('@condo/domains/user/utils/serverSchema/guards') @@ -274,15 +276,34 @@ const SendVoIPCallStartMessageService = new GQLCustomSchema('SendVoIPCallStartMe const callStatusJwtToken = buildCallStatusJWTToken(callStatusIdentityParams) // 4) Send messages - /** @type {Array>} */ + /** @type {Array>} */ const sendMessagePromises = verifiedResidentsWithContacts - .map(({ resident, contact, user }) => { - return sendMessageToUser({ - voipMessageType: VOIP_INCOMING_CALL_MESSAGE_TYPE, - context, resident, contact, user, property, customVoIPValues: customVoIPValuesByContactId[contact.id], - dv, sender, callData, b2cApp: { id: b2cAppId, name: b2cAppName }, - callStatusJwtToken, hasSendDTMFUrl, - }) + .map(async ({ resident, contact, user }) => { + + const voipType = getVoipTypeByData({ callData, customVoIPValues: customVoIPValuesByContactId[contact.id] }) + + const pushTypes = [ + VOIP_INCOMING_CALL_MESSAGE_TYPE, + voipType === NATIVE_VOIP_TYPE ? VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE : VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, + ] + const results = [] + + for (const pushType of pushTypes) { + let result = null + let error = null + try { + result = await sendMessageToUser({ + voipMessageType: pushType, + context, resident, contact, user, property, customVoIPValues: customVoIPValuesByContactId[contact.id], + dv, sender, callData, b2cApp: { id: b2cAppId, name: b2cAppName }, + callStatusJwtToken, hasSendDTMFUrl, + }) + } catch (err) { + error = err + } + results.push({ result, error }) + } + return { resident, contact, user, results } }) // 5) Set call status in redis diff --git a/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.spec.js b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.spec.js new file mode 100644 index 00000000000..e6b689b4087 --- /dev/null +++ b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.spec.js @@ -0,0 +1,196 @@ +const { faker } = require('@faker-js/faker') + +const APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE = faker.random.alphaNumeric(10) +const APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE = faker.random.alphaNumeric(10) + +require('@condo/domains/common/utils/testSchema/env').mockEnv({ + PUSH_ADAPTER_SETTINGS: () => { + const { VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE } = jest.requireActual('@condo/domains/notification/constants/constants') + + return { + pushTypeSwitchRulesByAppId: { + [APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE]: { + [VOIP_INCOMING_CALL_MESSAGE_TYPE]: 'enabled', + [VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE]: 'disabled', + [VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE]: 'disabled', + }, + }, + } + }, +}) + +const { + makeLoggedInAdminClient, + waitFor, + setFakeClientMode, +} = require('@open-condo/keystone/test.utils') + +const { + createTestB2CApp, + sendVoIPCallStartMessageByTestClient, + createTestB2CAppProperty, + createTestB2CAppAccessRight, + createTestB2CAppAccessRightSet, + createTestAppMessageSetting, + prepareVoIPUser, +} = require('@condo/domains/miniapp/utils/testSchema') +const { VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, MESSAGE_SENT_STATUS, DEVICE_PLATFORM_TYPES, PUSH_TRANSPORT_APPLE, PUSH_TRANSPORT_FIREBASE } = require('@condo/domains/notification/constants/constants') +const { Message, syncRemoteClientByTestClient } = require('@condo/domains/notification/utils/testSchema') +const { FLAT_UNIT_TYPE } = require('@condo/domains/property/constants/common') +const { makeClientWithResidentAccessAndProperty } = require('@condo/domains/property/utils/testSchema') +const { makeClientWithServiceUser } = require('@condo/domains/user/utils/testSchema') + +// eslint-disable-next-line import/order +const index = require('@app/condo') +const { getRandomPushTokenData, getRandomFakeSuccessToken } = require('../../notification/utils/testSchema/utils') + + +describe('SendVoIPCallStartMessageService spec', () => { + setFakeClientMode(index) + + let admin + + beforeAll(async () => { + admin = await makeLoggedInAdminClient() + }) + + describe('Logic', () => { + + let serviceUser + let organization + let property + let b2cApp + + beforeAll(async () => { + const [testB2CApp] = await createTestB2CApp(admin) + b2cApp = testB2CApp + + const { organization: testOrganization, property: testProperty } = await makeClientWithResidentAccessAndProperty() + organization = testOrganization + property = testProperty + await createTestB2CAppProperty(admin, b2cApp, { address: testProperty.address, addressMeta: testProperty.addressMeta }) + serviceUser = await makeClientWithServiceUser() + const [accessRightSet] = await createTestB2CAppAccessRightSet(admin, b2cApp, { canExecuteSendVoIPCallStartMessage: true }) + await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp, { accessRightSet: { connect: { id: accessRightSet.id } } }) + + await createTestAppMessageSetting(admin, { b2cApp, numberOfNotificationInWindow: 1000, type: VOIP_INCOMING_CALL_MESSAGE_TYPE }) + }) + + describe('Multiple messages for one call', () => { + + test('Send only one of messages to each appId of users RemoteClients', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + const actor = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) + + const fakeSuccessVoIPPushToken1 = getRandomPushTokenData({ token: getRandomFakeSuccessToken(), isVoIP: true, transport: PUSH_TRANSPORT_APPLE }) + const fakeSuccessVoIPPushToken2 = getRandomPushTokenData({ token: getRandomFakeSuccessToken(), isVoIP: true, transport: PUSH_TRANSPORT_FIREBASE }) + + await syncRemoteClientByTestClient(actor.userClient, { + appId: APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE, + deviceId: faker.datatype.uuid(), + devicePlatform: faker.helpers.arrayElement(DEVICE_PLATFORM_TYPES), + pushTokens: [fakeSuccessVoIPPushToken1], + }) + await syncRemoteClientByTestClient(actor.userClient, { + appId: APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE, + deviceId: faker.datatype.uuid(), + devicePlatform: faker.helpers.arrayElement(DEVICE_PLATFORM_TYPES), + pushTokens: [fakeSuccessVoIPPushToken2], + }) + + const callDataNative = { + callId: faker.datatype.uuid(), + nativeCallData: { + voipAddress: faker.internet.ip(), + voipLogin: faker.internet.userName(), + voipPassword: faker.internet.password(), + voipPanels: [{ dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }], + stunServers: [faker.internet.ip()], + codec: 'vp8', + }, + } + + const callDataB2c = { + callId: faker.datatype.uuid(), + b2cAppCallData: { + B2CAppContext: '', + }, + } + + const [resultNative] = await sendVoIPCallStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName, + unitType, + callData: callDataNative, + }) + + expect(resultNative.verifiedContactsCount).toBe(1) + expect(resultNative.createdMessagesCount).toBe(1) + expect(resultNative.erroredMessagesCount).toBe(0) + + const createdNativeMessages = await Message.getAll(admin, { + user: { id: actor.user.id }, + type_in: [VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE], + }, { first: 2, sortBy: ['createdAt_DESC'] }) + + const oldNativeVoIPMessage = createdNativeMessages.find(msg => msg.type === VOIP_INCOMING_CALL_MESSAGE_TYPE && msg.meta.data.callId === callDataNative.callId) + const newNativeVoIPMessage = createdNativeMessages.find(msg => msg.type === VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE && msg.meta.data.callId === callDataNative.callId) + expect(oldNativeVoIPMessage).toBeTruthy() + expect(newNativeVoIPMessage).toBeTruthy() + + + const [resultB2C] = await sendVoIPCallStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName, + unitType, + callData: callDataB2c, + }) + + expect(resultB2C.verifiedContactsCount).toBe(1) + expect(resultB2C.createdMessagesCount).toBe(1) + expect(resultB2C.erroredMessagesCount).toBe(0) + + const createdB2CMessages = await Message.getAll(admin, { + user: { id: actor.user.id }, + type_in: [VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE], + }, { first: 2, sortBy: ['createdAt_DESC'] }) + + const oldB2CVoIPMessage = createdB2CMessages.find(msg => msg.type === VOIP_INCOMING_CALL_MESSAGE_TYPE && msg.meta.data.callId === callDataB2c.callId) + const newB2CVoIPMessage = createdB2CMessages.find(msg => msg.type === VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE && msg.meta.data.callId === callDataB2c.callId) + expect(oldB2CVoIPMessage).toBeTruthy() + expect(newB2CVoIPMessage).toBeTruthy() + + async function waitForMessageResponses ({ messageId, successResponseAppId, failureResponseAppId }) { + await waitFor(async () => { + const sentOldNativeMessage = await Message.getOne(admin, { id: messageId }) + + const responses = sentOldNativeMessage.processingMeta.transportsMeta?.[0]?.deliveryMetadata?.responses + + const successResponse = responses?.find(r => r.success) + const failureResponse = responses?.find(r => !r.success) + + expect(sentOldNativeMessage.status).toEqual(MESSAGE_SENT_STATUS) + expect(successResponse).toBeTruthy() + expect(failureResponse).toBeTruthy() + expect(responses).toHaveLength(2) + + expect(successResponse.appId).toEqual(successResponseAppId) + expect(failureResponse.appId).toEqual(failureResponseAppId) + }, { interval: 1000, timeout: 5000 }) + } + + await waitForMessageResponses({ messageId: oldNativeVoIPMessage.id, successResponseAppId: APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE, failureResponseAppId: APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE }) + await waitForMessageResponses({ messageId: newNativeVoIPMessage.id, successResponseAppId: APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE, failureResponseAppId: APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE }) + await waitForMessageResponses({ messageId: oldB2CVoIPMessage.id, successResponseAppId: APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE, failureResponseAppId: APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE }) + await waitForMessageResponses({ messageId: newB2CVoIPMessage.id, successResponseAppId: APP_ID_DISABLED_VOIP_INCOMING_CALL_MESSAGE, failureResponseAppId: APP_ID_ENABLED_VOIP_INCOMING_CALL_MESSAGE }) + + }) + + }) + + }) + +}) \ No newline at end of file diff --git a/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.test.js b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.test.js index 414cebebf9e..2bf8170a052 100644 --- a/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.test.js +++ b/apps/condo/domains/miniapp/schema/SendVoIPCallStartMessageService.test.js @@ -24,10 +24,13 @@ const { createTestCustomValue, createTestAppMessageSetting, createTestCustomField, + prepareVoIPUser, } = require('@condo/domains/miniapp/utils/testSchema') const { getCallStatus, MAX_CALL_META_LENGTH, MAX_CALL_ID_LENGTH } = require('@condo/domains/miniapp/utils/voip') const { VOIP_INCOMING_CALL_MESSAGE_TYPE, + VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, + VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, } = require('@condo/domains/notification/constants/constants') const { Message } = require('@condo/domains/notification/utils/testSchema') const { FLAT_UNIT_TYPE } = require('@condo/domains/property/constants/common') @@ -41,26 +44,6 @@ const { makeClientWithSupportUser, makeClientWithNewRegisteredAndLoggedInUser, const { ERRORS } = require('./SendVoIPCallStartMessageService') -async function prepareUser ({ admin, organization, property, unitName, unitType }) { - const phone = createTestPhone() - const userClient = await makeClientWithResidentUser({}, { phone }) - const [contact] = await createTestContact(admin, organization, property, { - unitName: unitName, - unitType: unitType, - isVerified: true, - phone: phone, - }) - const [resident] = await createTestResident(admin, userClient.user, property, { - unitName: unitName, - unitType: unitType, - }) - - return { - user: userClient.user, - contact, - resident, - } -} describe('SendVoIPCallStartMessageService', () => { let admin @@ -429,7 +412,7 @@ describe('SendVoIPCallStartMessageService', () => { for (let i = 0; i < residentsCount; i++) { prepareDataPromises.push((async ({ admin, organization, property, unitName, unitType }) => { - const preparedUser = await prepareUser({ admin, organization, property, unitName, unitType }) + const preparedUser = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) preparedUsers.push(preparedUser) })({ admin, organization, property, unitName, unitType })) } @@ -465,7 +448,7 @@ describe('SendVoIPCallStartMessageService', () => { for (let i = 0; i < residentsCount; i++) { prepareDataPromises.push((async ({ admin, organization, property, unitName, unitType }) => { - const preparedUser = await prepareUser({ admin, organization, property, unitName, unitType }) + const preparedUser = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) preparedUsers.push(preparedUser) })({ admin, organization, property, unitName, unitType })) } @@ -563,7 +546,7 @@ describe('SendVoIPCallStartMessageService', () => { for (let i = 0; i < residentsCount; i++) { prepareDataPromises.push((async ({ admin, organization, property, unitName, unitType }) => { - const actor = await prepareUser({ admin, organization, property, unitName, unitType }) + const actor = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) actors.push(actor) })({ admin, organization, property, unitName, unitType })) } @@ -728,7 +711,7 @@ describe('SendVoIPCallStartMessageService', () => { test('uses native call voipType when CustomValue is absent', async () => { const unitName = faker.random.alphaNumeric(3) const unitType = FLAT_UNIT_TYPE - const actor = await prepareUser({ admin, organization, property, unitName, unitType }) + const actor = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) const callData = { callId: faker.datatype.uuid(), @@ -790,7 +773,7 @@ describe('SendVoIPCallStartMessageService', () => { await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp, { accessRightSet: { connect: { id: b2cRightSet.id } } }) - const actor = await prepareUser({ admin, organization, property, unitName, unitType: FLAT_UNIT_TYPE }) + const actor = await prepareVoIPUser({ admin, organization, property, unitName, unitType: FLAT_UNIT_TYPE }) const customValue = faker.random.alphaNumeric(8) @@ -843,5 +826,88 @@ describe('SendVoIPCallStartMessageService', () => { Object.fromEntries(customFieldNames.map(fieldName => ([fieldName, customValue]))) )) }) + + describe('Multiple messages for one call', () => { + + test(`Creates message of old ${VOIP_INCOMING_CALL_MESSAGE_TYPE} and one of the new types: ${VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE} or ${VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE}`, async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + const actor = await prepareVoIPUser({ admin, organization, property, unitName, unitType }) + + const callDataNative = { + callId: faker.datatype.uuid(), + nativeCallData: { + voipAddress: faker.internet.ip(), + voipLogin: faker.internet.userName(), + voipPassword: faker.internet.password(), + voipPanels: [{ dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }], + stunServers: [faker.internet.ip()], + codec: 'vp8', + }, + } + + const callDataB2c = { + callId: faker.datatype.uuid(), + b2cAppCallData: { + B2CAppContext: '', + }, + } + + const [resultNative] = await sendVoIPCallStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName, + unitType, + callData: callDataNative, + }) + + expect(resultNative.verifiedContactsCount).toBe(1) + expect(resultNative.createdMessagesCount).toBe(1) + expect(resultNative.erroredMessagesCount).toBe(0) + + const createdNativeMessages = await Message.getAll(admin, { + user: { id: actor.user.id }, + type_in: [VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE], + }, { first: 2, sortBy: ['createdAt_DESC'] }) + + expect(createdNativeMessages).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, + }), + expect.objectContaining({ + type: VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, + }), + ])) + + + const [resultB2C] = await sendVoIPCallStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName, + unitType, + callData: callDataB2c, + }) + + expect(resultB2C.verifiedContactsCount).toBe(1) + expect(resultB2C.createdMessagesCount).toBe(1) + expect(resultB2C.erroredMessagesCount).toBe(0) + + const createdB2CMessages = await Message.getAll(admin, { + user: { id: actor.user.id }, + type_in: [VOIP_INCOMING_CALL_MESSAGE_TYPE, VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE], + }, { first: 2, sortBy: ['createdAt_DESC'] }) + + expect(createdB2CMessages).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + }), + expect.objectContaining({ + type: VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, + }), + ])) + + }) + + }) }) }) \ No newline at end of file diff --git a/apps/condo/domains/miniapp/utils/sendVoIPCallMessage.js b/apps/condo/domains/miniapp/utils/sendVoIPCallMessage.js index 871d0f1eea5..0c5fdb9049e 100644 --- a/apps/condo/domains/miniapp/utils/sendVoIPCallMessage.js +++ b/apps/condo/domains/miniapp/utils/sendVoIPCallMessage.js @@ -22,6 +22,8 @@ const { MESSAGE_META, VOIP_INCOMING_CALL_MESSAGE_TYPE, MESSAGE_SENDING_STATUS, CANCELED_CALL_MESSAGE_PUSH_TYPE, + VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, + VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, } = require('@condo/domains/notification/constants/constants') const { sendMessage } = require('@condo/domains/notification/utils/serverSchema') const { getOldestNonDeletedProperty } = require('@condo/domains/property/utils/serverSchema/helpers') @@ -42,6 +44,8 @@ const POSSIBLE_CUSTOM_FIELD_NAMES_BY_MESSAGE_TYPE = VOIP_MESSAGE_TYPES.reduce((b const VOIP_MESSAGE_DATA_PREPARERS = { [VOIP_INCOMING_CALL_MESSAGE_TYPE]: prepareVoIPCallStartMessageData, + [VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE]: prepareVoIPCallStartMessageData, + [VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE]: prepareVoIPCallStartMessageData, [CANCELED_CALL_MESSAGE_PUSH_TYPE]: prepareVoIPCallCancelMessageData, } @@ -129,19 +133,7 @@ function prepareVoIPCallStartMessageData ({ callStatusJwtToken, } - const getVoIPCallStatusUrl = getGetVoIPCallStatusUrl({ dv: 1, sender, callStatusJwtToken }) - const getVoIPCallStatusTimeout = getGetVoIPCallStatusTimeout() - if (getVoIPCallStatusUrl && getVoIPCallStatusTimeout) { - preparedDataArgs = { - ...preparedDataArgs, - getVoIPCallStatusUrl, - getVoIPCallStatusTimeout, - } - } - - const isB2CAppCallDataIsOnlyOption = !callData.nativeCallData - const isB2CAppCallDataProvidedAndPreferredByCustomValue = !!callData.b2cAppCallData && customVoIPValues.voipType === B2C_APP_VOIP_TYPE - const needToPasteB2CAppCallData = isB2CAppCallDataIsOnlyOption || isB2CAppCallDataProvidedAndPreferredByCustomValue + const needToPasteB2CAppCallData = getVoipTypeByData({ callData, customVoIPValues }) === B2C_APP_VOIP_TYPE if (!needToPasteB2CAppCallData) { let voipType = customVoIPValues?.voipType || NATIVE_VOIP_TYPE @@ -242,7 +234,7 @@ function prepareVoIPCallCancelMessageData ({ async function sendMessageToUser (args) { const { voipMessageType, - context, resident, contact, user, + context, user, sender, dv, } = args @@ -268,8 +260,7 @@ async function sendMessageToUser (args) { }, } - const result = await sendMessage(context, messageAttrs) - return { resident, contact, user, result } + return await sendMessage(context, messageAttrs) } async function getVerifiedResidentsWithContacts ({ context, logContext, addressKey, unitName, unitType }) { @@ -412,22 +403,28 @@ async function getCustomVoIPValuesByContacts ({ context, contactIds, voipMessage } function parseSendMessageResults ({ logContext, sendMessagePromisesResults }) { - const sendMessageStats = sendMessagePromisesResults.map(promiseResult => { - if (promiseResult.status === 'rejected') { - return { success: false, error: promiseResult.reason } - } - - const { user, resident, contact, result } = promiseResult.value + const { rejectedResults, resolvedResults } = sendMessagePromisesResults.reduce((acc, promiseResult) => { + if (promiseResult.status === 'rejected') acc.rejectedResults.push(promiseResult) + else acc.resolvedResults.push(promiseResult) + return acc + }, { rejectedResults: [], resolvedResults: [] }) + + const rejectedMessagesStats = rejectedResults.map(promiseResult => ({ success: false, results: [{ error: promiseResult.reason }] })) + const resolvedMessagesStats = resolvedResults.map(({ value: { user, resident, contact, results } }) => { const userData = { userId: user.id, contactId: contact.id, residentId: resident.id } - const sendMessageResult = { id: result?.id, status: result?.status, isDuplicateMessage: result?.isDuplicateMessage } - if (result.isDuplicateMessage || result.status !== MESSAGE_SENDING_STATUS) { - return { ...userData, result: sendMessageResult, success: false } - } - return { ...userData, result: sendMessageResult, success: true } + const sendMessageResults = results.map(({ result, error }) => { + if (error) return { success: false, error: error instanceof Error ? error.message : String(error) } + if (!result) return { success: false, error: 'no result' } + return { success: !result.isDuplicateMessage && result.status === MESSAGE_SENDING_STATUS, result: { id: result?.id, status: result?.status, isDuplicateMessage: result?.isDuplicateMessage } } + }) + + return { ...userData, result: sendMessageResults, success: true } }) + const sendMessageStats = [...resolvedMessagesStats, ...rejectedMessagesStats] + if (!logContext) return sendMessageStats for (const messageStat of sendMessageStats) { @@ -532,6 +529,14 @@ function getInitialLogContext ({ addressKey, unitName, unitType, app, callData } } } +function getVoipTypeByData ({ callData, customVoIPValues }) { + const isB2CAppCallDataIsOnlyOption = !callData.nativeCallData + const isB2CAppCallDataProvidedAndPreferredByCustomValue = !!callData.b2cAppCallData && customVoIPValues?.voipType === B2C_APP_VOIP_TYPE + const needToPasteB2CAppCallData = isB2CAppCallDataIsOnlyOption || isB2CAppCallDataProvidedAndPreferredByCustomValue + + return needToPasteB2CAppCallData ? B2C_APP_VOIP_TYPE : NATIVE_VOIP_TYPE +} + module.exports = { getVerifiedResidentsWithContacts, getAppInfo, @@ -541,6 +546,7 @@ module.exports = { getLogInfoFn, checkLimits, getInitialLogContext, + getVoipTypeByData, RejectCallError, diff --git a/apps/condo/domains/miniapp/utils/testSchema/index.js b/apps/condo/domains/miniapp/utils/testSchema/index.js index a6ca03407c6..74bec987b85 100644 --- a/apps/condo/domains/miniapp/utils/testSchema/index.js +++ b/apps/condo/domains/miniapp/utils/testSchema/index.js @@ -956,6 +956,7 @@ async function prepareVoIPUser ({ admin, organization, property, unitName, unitT }) return { + userClient, user: userClient.user, contact, resident, diff --git a/apps/condo/domains/notification/constants/constants.js b/apps/condo/domains/notification/constants/constants.js index 52e8ddc905a..6e09b9ef9a0 100644 --- a/apps/condo/domains/notification/constants/constants.js +++ b/apps/condo/domains/notification/constants/constants.js @@ -61,6 +61,8 @@ const MOBILE_APP_UPDATE_AVAILABLE_MESSAGE_PUSH_TYPE = 'MOBILE_APP_UPDATE_AVAILAB const CUSTOM_CONTENT_MESSAGE_EMAIL_TYPE = 'CUSTOM_CONTENT_MESSAGE_EMAIL' const CUSTOM_CONTENT_MESSAGE_SMS_TYPE = 'CUSTOM_CONTENT_MESSAGE_SMS' const VOIP_INCOMING_CALL_MESSAGE_TYPE = 'VOIP_INCOMING_CALL_MESSAGE' +const VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE = 'VOIP_INCOMING_NATIVE_CALL_MESSAGE' +const VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE = 'VOIP_INCOMING_B2C_APP_CALL_MESSAGE' const CANCELED_CALL_MESSAGE_PUSH_TYPE = 'CANCELED_CALL_MESSAGE_PUSH' const RECURRENT_PAYMENT_PROCEEDING_SUCCESS_RESULT_MESSAGE_TYPE = 'RECURRENT_PAYMENT_PROCEEDING_SUCCESS_RESULT_MESSAGE' const RECURRENT_PAYMENT_PROCEEDING_UNKNOWN_ERROR_MESSAGE_TYPE = 'RECURRENT_PAYMENT_PROCEEDING_UNKNOWN_ERROR_MESSAGE' @@ -649,6 +651,38 @@ const MESSAGE_META = { sendDTMFTimeout: { required: false }, }, }, + [VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE]: { + dv: { required: true }, + title: { required: false }, + body: { required: true }, + data: { + B2CAppId: { required: true }, + B2CAppName: { required: true }, + residentId: { required: true }, + callId: { required: false }, + voipType: { required: false }, + voipAddress: { required: false }, + voipLogin: { required: false }, + voipPassword: { required: false }, + voipPanels: { required: false }, + stunServers: { required: false }, + codec: { required: false }, + address: { required: true }, + }, + }, + [VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE]: { + dv: { required: true }, + title: { required: false }, + body: { required: true }, + data: { + B2CAppId: { required: true }, + B2CAppContext: { required: false }, + B2CAppName: { required: true }, + residentId: { required: true }, + callId: { required: false }, + address: { required: true }, + }, + }, [CANCELED_CALL_MESSAGE_PUSH_TYPE]: { dv: { required: true }, title: { required: false }, @@ -995,6 +1029,22 @@ const MESSAGE_DELIVERY_OPTIONS = { isAllowedToChangeDefaultTransport: false, isVoIP: true, }, + [VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE]: { + // NOTE: Any realtime calls must be prioritized + priority: MESSAGE_DELIVERY_FAST_PRIORITY, + allowedTransports: [PUSH_TRANSPORT], + defaultTransports: [PUSH_TRANSPORT], + isAllowedToChangeDefaultTransport: false, + isVoIP: true, + }, + [VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE]: { + // NOTE: Any realtime calls must be prioritized + priority: MESSAGE_DELIVERY_FAST_PRIORITY, + allowedTransports: [PUSH_TRANSPORT], + defaultTransports: [PUSH_TRANSPORT], + isAllowedToChangeDefaultTransport: false, + isVoIP: true, + }, [CANCELED_CALL_MESSAGE_PUSH_TYPE]: { priority: MESSAGE_DELIVERY_FAST_PRIORITY, allowedTransports: [PUSH_TRANSPORT], @@ -1248,6 +1298,8 @@ module.exports = { MESSAGE_DELIVERY_FAST_PRIORITY, MESSAGE_DELIVERY_PRIORITY_TO_TASK_QUEUE_MAP, VOIP_INCOMING_CALL_MESSAGE_TYPE, + VOIP_INCOMING_NATIVE_CALL_MESSAGE_TYPE, + VOIP_INCOMING_B2C_APP_CALL_MESSAGE_TYPE, CANCELED_CALL_MESSAGE_PUSH_TYPE, B2C_APP_MESSAGE_PUSH_TYPE, APPLE_CONFIG_ENV, diff --git a/apps/condo/domains/notification/transports/push.js b/apps/condo/domains/notification/transports/push.js index baab324b5f8..9947cc4ce30 100644 --- a/apps/condo/domains/notification/transports/push.js +++ b/apps/condo/domains/notification/transports/push.js @@ -24,6 +24,7 @@ const { TICKET_COMMENT_CREATED_TYPE, PASS_TICKET_CREATED_MESSAGE_TYPE, PASS_TICKET_COMMENT_CREATED_MESSAGE_TYPE, + VOIP_INCOMING_CALL_MESSAGE_TYPE, } = require('@condo/domains/notification/constants/constants') const { renderTemplate } = require('@condo/domains/notification/templates') const { RemoteClient, RemoteClientPushToken } = require('@condo/domains/notification/utils/serverSchema') @@ -33,12 +34,15 @@ const { getTokens, groupIntoParallelGroupsWithSequentialBatches } = require('@co const logger = getLogger() +const PUSH_TYPE_RULE_ENABLED = 'enabled' +const PUSH_TYPE_RULE_DISABLED = 'disabled' /** * @typedef PushAdapterSettings * @property {Record | undefined} encryption - appId to encryptionVersion * @property {Record | undefined} groups - groupName to ordered appIds in group * @property {Record} transportPriorityByAppId - appId to transports array, if appId needs to be restricted to use specific transports + * @property {Record} pushTypeSwitchRulesByAppId - which push types to enable / disable globally or by appId of remote client */ /** @type {PushAdapterSettings} */ @@ -51,6 +55,15 @@ const TEMPORARY_DISABLED_TYPES_FOR_PUSH_NOTIFICATIONS = [ PASS_TICKET_COMMENT_CREATED_MESSAGE_TYPE, ] +const PUSH_TYPE_SWITCH_RULES_BY_APP_ID = { + ...PUSH_ADAPTER_SETTINGS.pushTypeSwitchRulesByAppId, + _global: { + ...Object.fromEntries(TEMPORARY_DISABLED_TYPES_FOR_PUSH_NOTIFICATIONS.map(messageType => ([messageType, PUSH_TYPE_RULE_DISABLED]))), + [VOIP_INCOMING_CALL_MESSAGE_TYPE]: PUSH_TYPE_RULE_DISABLED, + ...PUSH_ADAPTER_SETTINGS.pushTypeSwitchRulesByAppId?._global, + }, +} + const ADAPTERS = { [PUSH_TRANSPORT_FIREBASE]: new FirebaseAdapter(), [PUSH_TRANSPORT_REDSTORE]: new RedStoreAdapter(), @@ -301,10 +314,6 @@ async function send ({ notification, message, data, user, remoteClient } = {}, i const userId = get(user, 'id') const remoteClientId = get(remoteClient, 'id') - if (TEMPORARY_DISABLED_TYPES_FOR_PUSH_NOTIFICATIONS.includes(get(message, 'type'))) { - return [false, { error: 'Disabled type for push transport' }] - } - const pushTokens = await getTokens(userId, remoteClientId, isVoIP, PUSH_ADAPTER_SETTINGS.transportPriorityByAppId) // NOTE: For some message types with push transport, you need to override the push type for all push tokens. // If the message has a preferred push type, it takes priority over the value from the remote client. @@ -318,6 +327,26 @@ async function send ({ notification, message, data, user, remoteClient } = {}, i let container = { failureCount: 0, successCount: 0, responses: [] } let { recipients, statsInfo } = await prepareRecipients({ pushTokens, originalNotification: notification, originalData: data, message }) + + const disabledBySettingsRecipients = recipients.filter(({ appId }) => { + const appSettings = PUSH_TYPE_SWITCH_RULES_BY_APP_ID[appId]?.[message.type] + const globalSettings = PUSH_TYPE_SWITCH_RULES_BY_APP_ID._global?.[message.type] + if (appSettings === PUSH_TYPE_RULE_DISABLED) return true + if (globalSettings === PUSH_TYPE_RULE_DISABLED) { + return appSettings !== PUSH_TYPE_RULE_ENABLED + } + return false + }) + recipients = recipients.filter(recipient => !disabledBySettingsRecipients.includes(recipient)) + container.failureCount += disabledBySettingsRecipients.length + container.responses.push(...disabledBySettingsRecipients.map(recipient => ({ + success: false, + pushToken: recipient.token, + appId: recipient.appId, + pushType: recipient.pushType, + error: 'message type disabled by settings', + }))) + const invalidRecipients = recipients.filter(recipient => !recipient.notification || !recipient.data) recipients = recipients.filter(recipient => !invalidRecipients.includes(recipient)) diff --git a/apps/condo/lang/en/en.json b/apps/condo/lang/en/en.json index 8117290d1f7..7d7b93df953 100644 --- a/apps/condo/lang/en/en.json +++ b/apps/condo/lang/en/en.json @@ -1812,6 +1812,8 @@ "notification.messages.VERIFY_USER_EMAIL.email.subject": "Verification", "notification.messages.VERIFY_USER_EMAIL.email.title": "Verification", "notification.messages.VOIP_INCOMING_CALL_MESSAGE.push.title": "Intercom call", + "notification.messages.VOIP_INCOMING_NATIVE_CALL_MESSAGE.push.title": "Intercom call", + "notification.messages.VOIP_INCOMING_B2C_APP_CALL_MESSAGE.push.title": "Intercom call", "onboarding.tourStep.tooltip.ticket.message": "Make sure the information is correct and create an ticket", "onboarding.tourStep.tooltip.ticket.title": "Multiple fields filled automatically", "organization.createOrganizationForm.cancelModal.message": "You’ll need to create an organization to access the platform. Your progress will be lost if you leave this page.", diff --git a/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk b/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk b/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/en/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk b/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk b/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/en/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/es/es.json b/apps/condo/lang/es/es.json index 6197397dcd0..29a688354cf 100644 --- a/apps/condo/lang/es/es.json +++ b/apps/condo/lang/es/es.json @@ -1812,6 +1812,8 @@ "notification.messages.VERIFY_USER_EMAIL.email.subject": "Verificación", "notification.messages.VERIFY_USER_EMAIL.email.title": "Verificación", "notification.messages.VOIP_INCOMING_CALL_MESSAGE.push.title": "Llamada del interfono", + "notification.messages.VOIP_INCOMING_NATIVE_CALL_MESSAGE.push.title": "Llamada del interfono", + "notification.messages.VOIP_INCOMING_B2C_APP_CALL_MESSAGE.push.title": "Llamada del interfono", "onboarding.tourStep.tooltip.ticket.message": "Asegúrate de que la información es correcta y crea una incidencia", "onboarding.tourStep.tooltip.ticket.title": "Varios campos se completan automáticamente", "organization.createOrganizationForm.cancelModal.message": "El acceso a la plataforma solo estará disponible después de crear una organización. Si sales de esta página, los datos de la organización no se guardarán.", diff --git a/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk b/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk b/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/es/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk b/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk b/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/es/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk b/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk b/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/ru/messages/VOIP_INCOMING_B2C_APP_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk b/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk new file mode 100644 index 00000000000..b8561091c27 --- /dev/null +++ b/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/default.njk @@ -0,0 +1 @@ +{{ message.meta.body }} \ No newline at end of file diff --git a/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk b/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk new file mode 100644 index 00000000000..1a80f5377be --- /dev/null +++ b/apps/condo/lang/ru/messages/VOIP_INCOMING_NATIVE_CALL_MESSAGE/push.njk @@ -0,0 +1 @@ +{{ message.meta.data.address }} \ No newline at end of file diff --git a/apps/condo/lang/ru/ru.json b/apps/condo/lang/ru/ru.json index bf02a5af711..4fc0385b6c0 100644 --- a/apps/condo/lang/ru/ru.json +++ b/apps/condo/lang/ru/ru.json @@ -1812,6 +1812,8 @@ "notification.messages.VERIFY_USER_EMAIL.email.subject": "Верификация", "notification.messages.VERIFY_USER_EMAIL.email.title": "Верификация", "notification.messages.VOIP_INCOMING_CALL_MESSAGE.push.title": "Звонок домофона", + "notification.messages.VOIP_INCOMING_NATIVE_CALL_MESSAGE.push.title": "Звонок домофона", + "notification.messages.VOIP_INCOMING_B2C_APP_CALL_MESSAGE.push.title": "Звонок домофона", "onboarding.tourStep.tooltip.ticket.message": "Убедитесь, что информация верная, и создайте заявку", "onboarding.tourStep.tooltip.ticket.title": "Несколько полей заполнено автоматически", "organization.createOrganizationForm.cancelModal.message": "Вход на платформу будет доступен только после создания организации. Если вы уйдете с этой страницы, данные об организации не сохраняться.",