diff --git a/packages/destination-actions/src/destinations/braze/__tests__/braze.test.ts b/packages/destination-actions/src/destinations/braze/__tests__/braze.test.ts index 2a6210c1882..e79e5eb64cc 100644 --- a/packages/destination-actions/src/destinations/braze/__tests__/braze.test.ts +++ b/packages/destination-actions/src/destinations/braze/__tests__/braze.test.ts @@ -81,6 +81,102 @@ describe('Braze Cloud Mode (Actions)', () => { ]) }) }) + + it('should send subscription_groups with user profile updates', async () => { + nock('https://rest.iad-01.braze.com').post('/users/track').reply(200, {}) + + const event = createTestEvent({ + type: 'identify', + userId: 'user1234', + traits: { + email: 'test@example.com', + firstName: 'John', + subscription_groups: [ + { + subscription_group_id: 'newsletter_123', + subscription_state: 'subscribed' + }, + { + subscription_group_id: 'promotional_456', + subscription_state: 'unsubscribed' + } + ] + }, + receivedAt + }) + + const responses = await testDestination.testAction('updateUserProfile', { + event, + settings, + mapping: { + external_id: 'user1234', + email: 'test@example.com', + first_name: 'John', + subscription_groups: { '@path': '$.traits.subscription_groups'} + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + attributes: expect.arrayContaining([ + expect.objectContaining({ + external_id: 'user1234', + email: 'test@example.com', + first_name: 'John', + subscription_groups: [ + { + subscription_group_id: 'newsletter_123', + subscription_state: 'subscribed' + }, + { + subscription_group_id: 'promotional_456', + subscription_state: 'unsubscribed' + } + ] + }) + ]) + }) + }) + + it('should handle empty subscription_groups array', async () => { + nock('https://rest.iad-01.braze.com').post('/users/track').reply(200, {}) + + const event = createTestEvent({ + type: 'identify', + userId: 'user1234', + traits: { + email: 'test@example.com', + subscription_groups: [] + }, + receivedAt + }) + + const responses = await testDestination.testAction('updateUserProfile', { + event, + settings, + mapping: { + external_id: 'user1234', + email: 'test@example.com', + subscription_groups: { '@path': '$.traits.subscription_groups'} + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject({ + attributes: expect.arrayContaining([ + expect.objectContaining({ + external_id: 'user1234', + email: 'test@example.com' + }), + expect.not.objectContaining({ + subscription_groups: [] + }) + ]) + }) + }) }) describe('trackEvent', () => { diff --git a/packages/destination-actions/src/destinations/braze/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/braze/__tests__/multistatus.test.ts index 9ae45d53f10..563b41191ff 100644 --- a/packages/destination-actions/src/destinations/braze/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/braze/__tests__/multistatus.test.ts @@ -570,6 +570,9 @@ describe('MultiStatus', () => { }, braze_id: { '@path': '$.traits.brazeId' + }, + subscription_groups: { + '@path': '$.traits.subscription_groups' } } @@ -757,6 +760,86 @@ describe('MultiStatus', () => { } ]) }) + + it('should successfully handle a batch of events with subscription details, even if one fails validation', async () => { + nock(settings.endpoint).post('/users/track').reply(201, {}) + + const events: SegmentEvent[] = [ + // Valid Event + createTestEvent({ + type: 'identify', + receivedAt, + traits: { + firstName: 'Example', + lastName: 'User', + externalId: 'test-external-id', + subscription_groups: [ + { + subscription_group_id: 'newsletter_123', + subscription_state: 'subscribed' + }, + { + subscription_group_id: 'promotional_456', + subscription_state: 'unsubscribed' + } + ] + } + }), + createTestEvent({ + type: 'identify', + receivedAt, + traits: { + firstName: 'Example', + lastName: 'User', + email: 'user@example.com', + subscription_groups: [ + { + subscription_group_id: 'newsletter_9898' + } + ] + } + }), + // Event without any user identifier + createTestEvent({ + type: 'identify', + receivedAt, + traits: { + firstName: 'Example', + lastName: 'User', + email: 'user@example.com', + subscription_groups: [ + { + subscription_group_id: 'newsletter_9898', + subscription_state: 'unsubscribed' + } + ] + } + }) + ] + + const response = await testDestination.executeBatch('updateUserProfile', { + events, + settings, + mapping + }) + + expect(response[0]).toMatchObject({ + status: 200, + body: 'success' + }) + + expect(response[1]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'The value at /subscription_groups/0 is missing the required field \'subscription_state\'.', + errorreporter: 'INTEGRATIONS' + }) + + expect(response[2]).toMatchObject({ + status: 200, + body: 'success' + }) + }) }) describe('syncMode', () => { diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap index d191892fc20..3ff9b19b944 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap @@ -42,6 +42,12 @@ Object { "token": "M8rtOywDKgN((jhUBk9", }, ], + "subscription_groups": Array [ + Object { + "subscription_group_id": "M8rtOywDKgN((jhUBk9", + "subscription_state": "unsubscribed", + }, + ], "testType": "M8rtOywDKgN((jhUBk9", "time_zone": "M8rtOywDKgN((jhUBk9", "twitter": Object { diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile/generated-types.ts b/packages/destination-actions/src/destinations/braze/updateUserProfile/generated-types.ts index 7d6393ebb77..a950ccaaf27 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile/generated-types.ts @@ -130,6 +130,19 @@ export interface Payload { friends_count?: number statuses_count?: number } + /** + * Array of objects used to manage a user's subscription status for specific subscription groups. + */ + subscription_groups?: { + /** + * The identifier for the subscription group + */ + subscription_group_id: string + /** + * The user's subscription status: "subscribed" or "unsubscribed" + */ + subscription_state: string + }[] /** * Hash of custom attributes to send to Braze */ diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile/index.ts b/packages/destination-actions/src/destinations/braze/updateUserProfile/index.ts index 4ee56126380..fcb8c59d124 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile/index.ts +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile/index.ts @@ -266,6 +266,31 @@ const action: ActionDefinition = { } } }, + subscription_groups: { + label: 'Subscription Groups', + description: 'Array of objects used to manage a user\'s subscription status for specific subscription groups.', + type: 'object', + multiple: true, + defaultObjectUI: 'keyvalue', + properties: { + subscription_group_id: { + label: 'Subscription Group ID', + description: 'The identifier for the subscription group', + type: 'string', + required: true + }, + subscription_state: { + label: 'Subscription State', + description: 'The user\'s subscription status: "subscribed" or "unsubscribed"', + type: 'string', + required: true, + choices: [ + { label: 'Subscribed', value: 'subscribed' }, + { label: 'Unsubscribed', value: 'unsubscribed' } + ] + } + } + }, custom_attributes: { label: 'Custom Attributes', description: 'Hash of custom attributes to send to Braze', diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile2/generated-types.ts b/packages/destination-actions/src/destinations/braze/updateUserProfile2/generated-types.ts index 1547febef1b..d5457b8cd3c 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile2/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile2/generated-types.ts @@ -130,6 +130,19 @@ export interface Payload { friends_count?: number statuses_count?: number } + /** + * Array of objects used to manage a user's subscription status for specific subscription groups. + */ + subscription_groups?: { + /** + * The identifier for the subscription group + */ + subscription_group_id: string + /** + * The user's subscription status: "subscribed" or "unsubscribed" + */ + subscription_state: string + }[] /** * Hash of custom attributes to send to Braze */ diff --git a/packages/destination-actions/src/destinations/braze/updateUserProfile2/index.ts b/packages/destination-actions/src/destinations/braze/updateUserProfile2/index.ts index e9c798b17f0..111a7e2d0c7 100644 --- a/packages/destination-actions/src/destinations/braze/updateUserProfile2/index.ts +++ b/packages/destination-actions/src/destinations/braze/updateUserProfile2/index.ts @@ -273,6 +273,31 @@ const action: ActionDefinition = { } } }, + subscription_groups: { + label: 'Subscription Groups', + description: 'Array of objects used to manage a user\'s subscription status for specific subscription groups.', + type: 'object', + defaultObjectUI: 'keyvalue', + multiple: true, + properties: { + subscription_group_id: { + label: 'Subscription Group ID', + description: 'The identifier for the subscription group', + type: 'string', + required: true + }, + subscription_state: { + label: 'Subscription State', + description: 'The user\'s subscription status: "subscribed" or "unsubscribed"', + type: 'string', + required: true, + choices: [ + { label: 'Subscribed', value: 'subscribed' }, + { label: 'Unsubscribed', value: 'unsubscribed' } + ] + } + } + }, custom_attributes: { label: 'Custom Attributes', description: 'Hash of custom attributes to send to Braze', diff --git a/packages/destination-actions/src/destinations/braze/utils.ts b/packages/destination-actions/src/destinations/braze/utils.ts index 0a9869051c9..8f08581bb1a 100644 --- a/packages/destination-actions/src/destinations/braze/utils.ts +++ b/packages/destination-actions/src/destinations/braze/utils.ts @@ -433,6 +433,7 @@ export function updateUserProfile( push_tokens: payload.push_tokens, time_zone: payload.time_zone, twitter: payload.twitter, + ...(typeof payload.subscription_groups === 'object' && Array.isArray(payload.subscription_groups) && payload.subscription_groups.length>0 ? { subscription_groups: payload.subscription_groups } : {}), _update_existing_only: updateExistingOnly } ] @@ -513,6 +514,7 @@ export async function updateBatchedUserProfile( push_tokens: payload.push_tokens, time_zone: payload.time_zone, twitter: payload.twitter, + ...(typeof payload.subscription_groups === 'object' && Array.isArray(payload.subscription_groups) && payload.subscription_groups.length>0 ? { subscription_groups: payload.subscription_groups } : {}), _update_existing_only: updateExistingOnly } diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/generated-types.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/generated-types.ts index e7be7fdd068..8156784a662 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/generated-types.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/generated-types.ts @@ -56,7 +56,7 @@ export interface Payload { */ validityPeriod?: number /** - * The time that Twilio will send the message. Must be in ISO 8601 format. + * The time that Twilio will send the message. Must be in ISO 8601 format. Messages can be scheduled up to 35 days in advance, and at least 15 minutes in advance. */ sendAt?: string /**