diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 85b600ee99..1b45b8e771 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -18,6 +18,8 @@ import triggerCanvas from './triggerCanvas' import { EVENT_NAMES } from './ecommerce/constants' import upsertCatalogItem from './upsertCatalogItem' +import mergeUsers from './mergeUsers' + const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', slug: 'actions-braze-cloud', @@ -96,12 +98,14 @@ const destination: DestinationDefinition = { triggerCampaign, triggerCanvas, ecommerce, - ecommerceSingleProduct + ecommerceSingleProduct, + mergeUsers }, presets: [ { name: 'Track Calls', - subscribe: 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', + subscribe: + 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields), type: 'automatic' @@ -110,9 +114,9 @@ const destination: DestinationDefinition = { name: 'Order Placed (beta)', subscribe: 'event = "Order Completed"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_PLACED, + name: EVENT_NAMES.ORDER_PLACED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -123,9 +127,9 @@ const destination: DestinationDefinition = { name: 'Checkout Started (beta)', subscribe: 'event = "Checkout Started"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.CHECKOUT_STARTED, + name: EVENT_NAMES.CHECKOUT_STARTED, metadata: { checkout_url: { '@path': '$.properties.checkout_url' } } @@ -136,9 +140,9 @@ const destination: DestinationDefinition = { name: 'Order Refunded (beta)', subscribe: 'event = "Order Refunded"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_REFUNDED, + name: EVENT_NAMES.ORDER_REFUNDED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -149,9 +153,9 @@ const destination: DestinationDefinition = { name: 'Order Cancelled (beta)', subscribe: 'event = "Order Cancelled"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_CANCELLED, + name: EVENT_NAMES.ORDER_CANCELLED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -162,7 +166,7 @@ const destination: DestinationDefinition = { name: 'Product Viewed (beta)', subscribe: 'event = "Product Viewed"', partnerAction: 'ecommerceSingleProduct', - mapping: { + mapping: { ...defaultValues(ecommerceSingleProduct.fields), name: EVENT_NAMES.PRODUCT_VIEWED }, diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..91c9b17acf --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Braze's mergeUsers destination action: all fields 1`] = ` +Object { + "merge_updates": Array [ + Object { + "identifier_to_keep": Object { + "braze_id": "8$QYsxcxpjIn)Y", + "email": "gonwijpel@vep.il", + "external_id": "8$QYsxcxpjIn)Y", + "phone": "8$QYsxcxpjIn)Y", + "user_alias": Object { + "alias_label": "segment", + "alias_name": "keep-alias", + }, + }, + "identifier_to_merge": Object { + "braze_id": "8$QYsxcxpjIn)Y", + "email": "gonwijpel@vep.il", + "external_id": "8$QYsxcxpjIn)Y", + "phone": "8$QYsxcxpjIn)Y", + "user_alias": Object { + "alias_label": "segment", + "alias_name": "merge-alias", + }, + }, + }, + ], +} +`; + +exports[`Testing snapshot for Braze's mergeUsers destination action: required fields 1`] = ` +Object { + "merge_updates": Array [ + Object { + "identifier_to_keep": Object { + "external_id": "user-to-keep", + }, + "identifier_to_merge": Object { + "external_id": "user-to-merge", + }, + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts new file mode 100644 index 0000000000..fc39bae300 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/index.test.ts @@ -0,0 +1,355 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const settings = { + app_id: 'my-app-id', + api_key: 'my-api-key', + endpoint: 'https://rest.iad-01.braze.com' +} + +describe('Braze.mergeUsers', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('should merge users with external_id identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({ message: 'success' }) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) + + it('should merge users with user_alias identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + ] + }) + }) + + it('should merge users with email identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + email: 'keep@example.com' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + email: 'keep@example.com' + } + } + ] + }) + }) + + it('should merge users with braze_id identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + braze_id: 'braze-merge-id-123' + }, + identifier_to_keep: { + braze_id: 'braze-keep-id-456' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + braze_id: 'braze-merge-id-123' + }, + identifier_to_keep: { + braze_id: 'braze-keep-id-456' + } + } + ] + }) + }) + + it('should merge users with phone identifiers', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + phone: '+14155551234' + }, + identifier_to_keep: { + phone: '+14155555678' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + phone: '+14155551234' + }, + identifier_to_keep: { + phone: '+14155555678' + } + } + ] + }) + }) + + it('should merge users with mixed identifier types', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + email: 'merge@example.com' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) + + it('should throw error when identifier_to_merge has no valid identifier', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: {}, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + ).rejects.toThrowError( + 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should throw error when identifier_to_keep has no valid identifier', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: {} + } + }) + ).rejects.toThrowError( + 'Identifier to Keep must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should handle incomplete user_alias (missing required fields)', async () => { + const event = createTestEvent({ + type: 'track' + }) + + await expect( + testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + user_alias: { + alias_name: 'test' + // missing alias_label + } + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + }) + ).rejects.toThrowError( + 'Identifier to Merge must specify one of: external_id, user_alias, braze_id, email, or phone.' + ) + }) + + it('should use default mapping from userId for identifier_to_keep', async () => { + nock(settings.endpoint).post('/users/merge').reply(200, { message: 'success' }) + + const event = createTestEvent({ + type: 'track', + userId: 'user-to-keep-123' + }) + + const responses = await testDestination.testAction('mergeUsers', { + event, + settings, + mapping: { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: { + '@path': '$.userId' + } + } + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + merge_updates: [ + { + identifier_to_merge: { + external_id: 'user-to-merge-456' + }, + identifier_to_keep: { + external_id: 'user-to-keep-123' + } + } + ] + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..263bd2efb7 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/__tests__/snapshot.test.ts @@ -0,0 +1,110 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'mergeUsers' +const destinationSlug = 'Braze' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [, settingsData] = generateTestData(seedName, destination, action, true) + + // Provide custom event data with valid identifiers + const customEventData = { + identifier_to_merge: { + external_id: 'user-to-merge' + }, + identifier_to_keep: { + external_id: 'user-to-keep' + } + } + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: customEventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: customEventData, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + // Customize the nested objects to include all identifier types + const customEventData = { + ...eventData, + identifier_to_merge: { + external_id: eventData.identifier_to_merge?.external_id || 'merge-external-id', + braze_id: eventData.identifier_to_merge?.braze_id || 'merge-braze-id', + email: eventData.identifier_to_merge?.email || 'merge@example.com', + phone: eventData.identifier_to_merge?.phone || '+14155551234', + user_alias: { + alias_name: 'merge-alias', + alias_label: 'segment' + } + }, + identifier_to_keep: { + external_id: eventData.identifier_to_keep?.external_id || 'keep-external-id', + braze_id: eventData.identifier_to_keep?.braze_id || 'keep-braze-id', + email: eventData.identifier_to_keep?.email || 'keep@example.com', + phone: eventData.identifier_to_keep?.phone || '+14155555678', + user_alias: { + alias_name: 'keep-alias', + alias_label: 'segment' + } + } + } + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: customEventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: customEventData, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts new file mode 100644 index 0000000000..02845070f7 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/generated-types.ts @@ -0,0 +1,46 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The type of identifier for the user to be merged. One of: external_id, user_alias, braze_id, email, or phone. + */ + previousIdType: string + /** + * The value of the identifier for the user to be merged. + */ + previousIdValue?: string + /** + * The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias. + */ + previousAliasIdValue?: { + /** + * The label of the user alias for the user to be merged. + */ + alias_label: string + /** + * The name of the user alias for the user to be merged. + */ + alias_name: string + } + /** + * The type of identifier for the user to be kept. One of: external_id, user_alias, braze_id, email, or phone. + */ + keepIdType: string + /** + * The value of the identifier for the user to be kept. + */ + keepIdValue?: string + /** + * The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias. + */ + keepAliasIdValue?: { + /** + * The label of the user alias for the user to be kept. + */ + alias_label: string + /** + * The name of the user alias for the user to be kept. + */ + alias_name: string + } +} diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts new file mode 100644 index 0000000000..48de156c6f --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/index.ts @@ -0,0 +1,242 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { mergeUsers } from '../utils' + +const prioritizationChoices = [ + { value: 'identified', label: 'Identified' }, + { value: 'unidentified', label: 'Unidentified' }, + { value: 'most_recently_updated', label: 'Most Recently Updated' }, + { value: 'least_recently_updated', label: 'Least Recently Updated' } +] + +const action: ActionDefinition = { + title: 'Merge Users', + description: + 'Merge one identified user into another identified user. The merge will occur asynchronously and can take between 5-10 minutes.', + defaultSubscription: 'type = "alias"', + fields: { + previousIdType: { + label: 'Type of Identifier to merge', + description: + 'The type of identifier for the user to be merged. One of: external_id, user_alias, email, or phone.', + type: 'string', + required: true, + choices: [ + { label: 'External ID', value: 'external_id' }, + { label: 'User Alias', value: 'user_alias' }, + { label: 'Email', value: 'email' }, + { label: 'Phone', value: 'phone' } + ], + default: { + '@path': 'external_id' + } + }, + previousIdValue: { + label: 'ID value to merge', + description: 'The value of the identifier for the user to be merged.', + type: 'string', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is_not', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is_not', + value: 'user_alias' + } + ] + }, + default: { + '@path': '$.previousId' + } + }, + previousAliasIdValue: { + label: 'User Alias value to merge', + description: + 'The value of the user alias identifier for the user to be merged. Required if the previous identifier type is user_alias.', + type: 'object', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + properties: { + alias_label: { + label: 'User Alias Label', + description: 'The label of the user alias for the user to be merged.', + type: 'string', + required: true + }, + alias_name: { + label: 'User Alias Name', + description: 'The name of the user alias for the user to be merged.', + type: 'string', + required: true + } + } + }, + keepIdType: { + label: 'Type of Identifier to keep', + description: 'The type of identifier for the user to be kept. One of: external_id, user_alias, email, or phone.', + type: 'string', + required: true, + choices: [ + { label: 'External ID', value: 'external_id' }, + { label: 'User Alias', value: 'user_alias' }, + { label: 'Email', value: 'email' }, + { label: 'Phone', value: 'phone' } + ], + default: 'external_id' + }, + keepIdValue: { + label: 'ID value to keep', + description: 'The value of the identifier for the user to be kept.', + type: 'string', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is_not', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is_not', + value: 'user_alias' + } + ] + }, + default: 'external_id' + }, + keepAliasIdValue: { + label: 'User Alias value to keep', + description: + 'The value of the user alias identifier for the user to be kept. Required if the keep identifier type is user_alias.', + type: 'object', + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: 'user_alias' + } + ] + }, + properties: { + alias_label: { + label: 'User Alias Label', + description: 'The label of the user alias for the user to be kept.', + type: 'string', + required: true + }, + alias_name: { + label: 'User Alias Name', + description: 'The name of the user alias for the user to be kept.', + type: 'string', + required: true + } + } + }, + keepIdPrioritization: { + label: 'Rule Prioritization', + description: 'Rule determining which user to merge if multiple users are found.', + type: 'string', + choices: prioritizationChoices, + required: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'keepIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + default: 'identified' + }, + previousIdPrioritization: { + label: 'Rule Prioritization', + description: 'Rule determining which user to merge if multiple users are found.', + type: 'string', + choices: prioritizationChoices, + required: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + depends_on: { + match: 'all', + conditions: [ + { + fieldKey: 'previousIdType', + operator: 'is', + value: ['email', 'phone'] + } + ] + }, + default: 'identified' + } + }, + perform: (request, { settings, payload }) => { + return mergeUsers(request, settings, payload) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts new file mode 100644 index 0000000000..55db4a62b1 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/mergeUsers/types.ts @@ -0,0 +1,26 @@ +export type MergeUsersJSON = { + identifier_to_merge: { + // Only one of the following + external_id?: string + user_alias?: { + alias_label: string + alias_name: string + } + email?: string + phone?: string + previousIdPrioritization?: string + } + identifier_to_keep: { + // Only one of the following + external_id?: string + user_alias?: { + alias_label: string + alias_name: string + } + email?: string + phone?: string + keepIdPrioritization?: string + } +} + +export type MergeIdentifierType = 'external_id' | 'user_alias' | 'email' | 'phone' diff --git a/packages/destination-actions/src/destinations/braze/userAlias.ts b/packages/destination-actions/src/destinations/braze/userAlias.ts index 285b6cecd7..081b122615 100644 --- a/packages/destination-actions/src/destinations/braze/userAlias.ts +++ b/packages/destination-actions/src/destinations/braze/userAlias.ts @@ -1,4 +1,4 @@ -interface UserAlias { +export interface UserAlias { alias_name: string alias_label: string } diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index e9a863cefb..b0b7e93255 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -1,4 +1,4 @@ -import { JSONLikeObject, ModifiedResponse, MultiStatusResponse, omit } from '@segment/actions-core' +import { JSONLikeObject, ModifiedResponse, MultiStatusResponse, omit, PayloadValidationError } from '@segment/actions-core' import { IntegrationError, RequestClient, removeUndefined } from '@segment/actions-core' import dayjs from 'dayjs' import { Settings } from './generated-types' @@ -6,7 +6,9 @@ import action from './trackPurchase' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import { Payload as TrackPurchasePayload } from './trackPurchase/generated-types' import { Payload as UpdateUserProfilePayload } from './updateUserProfile/generated-types' -import { getUserAlias } from './userAlias' +import { Payload as MergeUsersPayload } from './mergeUsers/generated-types' +import { MergeUsersJSON, MergeIdentifierType } from './mergeUsers/types' +import { getUserAlias, UserAlias } from './userAlias' import { HTTPError } from '@segment/actions-core' import { MAX_BATCH_SIZE } from './constants' type DateInput = string | Date | number | null | undefined @@ -659,6 +661,49 @@ async function handleBrazeAPIResponse( } } +export function mergeUsers(request: RequestClient, settings: Settings, payload: MergeUsersPayload) { + const { + previousIdType, + previousIdValue, + previousAliasIdValue, + keepIdType, + keepIdValue, + keepAliasIdValue + } = payload + + const previousId = getMergeIdentifier(previousIdType as MergeIdentifierType, 'merge', previousIdValue, previousAliasIdValue) + const keepId = getMergeIdentifier(keepIdType as MergeIdentifierType, 'keep', keepIdValue, keepAliasIdValue) + + const mergeUpdate: MergeUsersJSON = { + identifier_to_merge: { [previousIdType]: previousId }, + identifier_to_keep: { [keepIdType]: keepId } + } + + return request(`${settings.endpoint}/users/merge`, { + method: 'post', + json: { + merge_updates: [mergeUpdate] + } + }) +} + +export function getMergeIdentifier(type: MergeIdentifierType, label: string, value?: string, aliasValue?: UserAlias): string | UserAlias { + if (type === 'user_alias') { + const { alias_label, alias_name } = aliasValue || {} + if (!alias_label || !alias_name) { + throw new PayloadValidationError(`When Type of Identifier to ${label} is user_alias, alias_label and alias_name must be provided.`) + } + return { alias_label, alias_name } + } + + if (!value) { + throw new PayloadValidationError(`ID value to ${label} value must be provided.`) + } + + return value +} + + export function generateMultiStatusError(batchSize: number, errorMessage: string): MultiStatusResponse { const multiStatusResponse = new MultiStatusResponse()