From 37fdeb0cd45e454c6be3c6e5b6a96d2be6607124 Mon Sep 17 00:00:00 2001 From: harsh-joshi99 <129737395+harsh-joshi99@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:22:14 +0530 Subject: [PATCH] Add profile to list in upsertProfile (#1706) * Add profile to list in upsertProfile -> Add list Id mapping in upsertProfile action -> Add profile to list if list Id is provided -> Update test case * Update snapshot tests * Update mapping description --- .../klaviyo/__tests__/snapshot.test.ts | 5 +- .../src/destinations/klaviyo/functions.ts | 50 +++++++ .../src/destinations/klaviyo/types.ts | 9 ++ .../upsertProfile/__tests__/index.test.ts | 124 ++++++++++++++++++ .../upsertProfile/__tests__/snapshot.test.ts | 5 +- .../klaviyo/upsertProfile/generated-types.ts | 4 + .../klaviyo/upsertProfile/index.ts | 25 +++- 7 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 packages/destination-actions/src/destinations/klaviyo/functions.ts diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts index 08427e84d2..da750935bf 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts @@ -14,7 +14,10 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => { const [eventData, settingsData] = generateTestData(seedName, destination, action, false) nock(/.*/).persist().get(/.*/).reply(200) - nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/) + .persist() + .post(/.*/) + .reply(200, { data: { id: 'fake-id' } }) nock(/.*/).persist().put(/.*/).reply(200) const event = createTestEvent({ diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts new file mode 100644 index 0000000000..3801495e37 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -0,0 +1,50 @@ +import { RequestClient, DynamicFieldResponse, APIError } from '@segment/actions-core' +import { API_URL, REVISION_DATE } from './config' +import { GetListResultContent } from './types' +import { Settings } from './generated-types' + +export async function getListIdDynamicData(request: RequestClient, settings: Settings): Promise { + try { + const result = await request(`${API_URL}/lists/`, { + method: 'get', + headers: { + Authorization: `Klaviyo-API-Key ${settings.api_key}`, + Accept: 'application/json', + revision: REVISION_DATE + }, + skipResponseCloning: true + }) + const parsedContent = JSON.parse(result.content) as GetListResultContent + const choices = parsedContent.data.map((list: { id: string; attributes: { name: string } }) => { + return { value: list.id, label: list.attributes.name } + }) + return { + choices + } + } catch (err) { + return { + choices: [], + nextPage: '', + error: { + message: (err as APIError).message ?? 'Unknown error', + code: (err as APIError).status + '' ?? 'Unknown error' + } + } + } +} + +export async function addProfileToList(request: RequestClient, profileId: string, listId: string) { + const listData = { + data: [ + { + type: 'profile', + id: profileId + } + ] + } + + await request(`${API_URL}/lists/${listId}/relationships/profiles/`, { + method: 'POST', + json: listData + }) +} diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index aaad24910a..57ad5a07df 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -62,3 +62,12 @@ export interface EventData { } } } + +export interface GetListResultContent { + data: { + id: string + attributes: { + name: string + } + }[] +} diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts index 0a9c9636bb..ccfdd0897e 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts @@ -2,6 +2,7 @@ import nock from 'nock' import { IntegrationError, createTestEvent, createTestIntegration } from '@segment/actions-core' import Definition from '../../index' import { API_URL } from '../../config' +import * as Functions from '../../functions' const testDestination = createTestIntegration(Definition) @@ -11,7 +12,16 @@ export const settings = { api_key: apiKey } +jest.mock('../../functions', () => ({ + ...jest.requireActual('../../functions'), + addProfileToList: jest.fn(() => Promise.resolve()) +})) + describe('Upsert Profile', () => { + afterEach(() => { + jest.resetAllMocks() + }) + it('should throw error if no email, phone_number, or external_id is provided', async () => { const event = createTestEvent({ type: 'track', @@ -138,4 +148,118 @@ describe('Upsert Profile', () => { testDestination.testAction('upsertProfile', { event, settings, useDefaultMappings: true }) ).rejects.toThrowError('An error occurred while processing the request') }) + + it('should add a profile to a list if list_id is provided', async () => { + const listId = 'abc123' + const profileId = '123' + const mapping = { list_id: 'abc123' } + + const requestBody = { + data: { + type: 'profile', + attributes: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + location: {}, + properties: {} + } + } + } + + nock(`${API_URL}`) + .post('/profiles/', requestBody) + .reply( + 200, + JSON.stringify({ + data: { + id: profileId + } + }) + ) + + nock(`${API_URL}`).post(`/lists/${listId}/relationships/profiles/`).reply(200) + + const event = createTestEvent({ + type: 'track', + userId: '123', + traits: { + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + list_id: listId + } + }) + + await expect( + testDestination.testAction('upsertProfile', { event, mapping, settings, useDefaultMappings: true }) + ).resolves.not.toThrowError() + + expect(Functions.addProfileToList).toHaveBeenCalledWith(expect.anything(), profileId, listId) + }) + + it('should add an existing profile to a list if list_id is provided', async () => { + const listId = 'abc123' + const profileId = '123' + const mapping = { list_id: 'abc123' } + + const requestBody = { + data: { + type: 'profile', + attributes: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + location: {}, + properties: {} + } + } + } + + const errorResponse = JSON.stringify({ + errors: [ + { + meta: { + duplicate_profile_id: profileId + } + } + ] + }) + + nock(`${API_URL}`).post('/profiles/', requestBody).reply(409, errorResponse) + + const updateRequestBody = { + data: { + type: 'profile', + id: profileId, + attributes: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + location: {}, + properties: {} + } + } + } + nock(`${API_URL}`).patch(`/profiles/${profileId}`, updateRequestBody).reply(200, {}) + + nock(`${API_URL}`).post(`/lists/${listId}/relationships/profiles/`).reply(200) + + const event = createTestEvent({ + type: 'track', + userId: '123', + traits: { + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + list_id: listId + } + }) + + await expect( + testDestination.testAction('upsertProfile', { event, mapping, settings, useDefaultMappings: true }) + ).resolves.not.toThrowError() + + expect(Functions.addProfileToList).toHaveBeenCalledWith(expect.anything(), profileId, listId) + }) }) diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/snapshot.test.ts index 87817178b6..99ab6d2182 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/snapshot.test.ts @@ -14,7 +14,10 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const [eventData, settingsData] = generateTestData(seedName, destination, action, false) nock(/.*/).persist().get(/.*/).reply(200) - nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/) + .persist() + .post(/.*/) + .reply(200, { data: { id: 'fake-id' } }) nock(/.*/).persist().put(/.*/).reply(200) const event = createTestEvent({ diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts index 1de9efa3ed..8c359e5c39 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts @@ -52,4 +52,8 @@ export interface Payload { properties?: { [k: string]: unknown } + /** + * The Klaviyo list to add the profile to. + */ + list_id?: string } diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts index 5bb6846c73..396b967eda 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts @@ -1,10 +1,11 @@ -import type { ActionDefinition } from '@segment/actions-core' +import type { ActionDefinition, DynamicFieldResponse } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { API_URL } from '../config' import { APIError, PayloadValidationError } from '@segment/actions-core' import { KlaviyoAPIError, ProfileData } from '../types' +import { addProfileToList, getListIdDynamicData } from '../functions' const action: ActionDefinition = { title: 'Upsert Profile', @@ -120,10 +121,21 @@ const action: ActionDefinition = { default: { '@path': '$.properties' } + }, + list_id: { + label: 'List', + description: `The Klaviyo list to add the profile to.`, + type: 'string', + dynamic: true + } + }, + dynamicFields: { + list_id: async (request, { settings }): Promise => { + return getListIdDynamicData(request, settings) } }, perform: async (request, { payload }) => { - const { email, external_id, phone_number, ...otherAttributes } = payload + const { email, external_id, phone_number, list_id, ...otherAttributes } = payload if (!email && !phone_number && !external_id) { throw new PayloadValidationError('One of External ID, Phone Number and Email is required.') @@ -146,6 +158,11 @@ const action: ActionDefinition = { method: 'POST', json: profileData }) + if (list_id) { + const content = JSON.parse(profile?.content) + const id = content.data.id + await addProfileToList(request, id, list_id) + } return profile } catch (error) { const { response } = error as KlaviyoAPIError @@ -161,6 +178,10 @@ const action: ActionDefinition = { method: 'PATCH', json: profileData }) + + if (list_id) { + await addProfileToList(request, id, list_id) + } return profile } }