diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap index b289febcd40..da92733b015 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap @@ -9,21 +9,15 @@ Object { "braze_id": "tZlmdH(v3%S", "email": "rabavori@fo.sx", "external_id": "tZlmdH(v3%S", - "name": "ecommerce.order_placed", + "name": "ecommerce.checkout_started", "phone": "tZlmdH(v3%S", "properties": Object { "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", "currency": "HTG", - "discounts": Array [ - Object { - "amount": -17122291498352.64, - "code": "tZlmdH(v3%S", - }, - ], "metadata": Object { "testType": "tZlmdH(v3%S", }, - "order_id": "tZlmdH(v3%S", "products": Array [ Object { "image_url": "http://vaci.va/inu", @@ -35,8 +29,10 @@ Object { "variant_id": "tZlmdH(v3%S", }, ], + "shipping": -17122291498352.64, "source": "tZlmdH(v3%S", - "total_discounts": -17122291498352.64, + "subtotal_value": -17122291498352.64, + "tax": -17122291498352.64, "total_value": -17122291498352.64, }, "time": "2025-01-01T00:00:00.000Z", @@ -56,11 +52,11 @@ Object { "app_id": "tZlmdH(v3%S", "braze_id": "tZlmdH(v3%S", "external_id": "tZlmdH(v3%S", - "name": "ecommerce.order_placed", + "name": "ecommerce.checkout_started", "properties": Object { "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", "currency": "HTG", - "order_id": "tZlmdH(v3%S", "products": Array [ Object { "price": -17122291498352.64, diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts index 6a41c48900e..f9c2df910bf 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts @@ -30,6 +30,10 @@ const payload = { cart_id: 'cart_id_1', checkout_id: 'checkout_id_1', total: 100.0, + subtotal: 85.0, + tax: 9.0, + shipping: 6.0, + action: 'replace', discount: 10, discount_items: [ { @@ -99,6 +103,10 @@ const mapping = { order_id: { '@path': '$.properties.order_id' }, cart_id: { '@path': '$.properties.cart_id' }, total_value: { '@path': '$.properties.total' }, + action: { '@path': '$.properties.action' }, + subtotal_value: { '@path': '$.properties.subtotal' }, + tax: { '@path': '$.properties.tax' }, + shipping: { '@path': '$.properties.shipping' }, total_discounts: { '@path': '$.properties.discount' }, discounts: { '@path': '$.properties.discount_items' }, currency: { '@path': '$.properties.currency' }, @@ -192,12 +200,15 @@ describe('Braze.ecommerce', () => { ], total_value: 100, order_id: 'order_id_1', + cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, { code: 'VIPCUSTOMER', amount: 5 } ], - cart_id: 'cart_id_1', metadata: { custom_field_1: 'custom_value_1', custom_field_2: 100, @@ -279,6 +290,9 @@ describe('Braze.ecommerce', () => { total_value: 100, checkout_id: 'checkout_id_1', cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6, metadata: { custom_field_1: 'custom_value_1', custom_field_2: 100, @@ -441,6 +455,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: 'order_id_1', cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, @@ -473,6 +490,174 @@ describe('Braze.ecommerce', () => { expect(response.length).toBe(1) }) + it('should send Cart Updated event with all fields correctly', async () => { + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED + } + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) + delete e.properties?.product + + const json = { + events: [ + { + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + products: [ + { + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + quantity: 2, + price: 25, + metadata: { + color: 'red', + size: 'M' + } + }, + { + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: 'cart_id_1', + action: 'replace', + subtotal_value: 85, + tax: 9, + shipping: 6, + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' + } + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint).post('/users/track', json).reply(200) + + const response = await testDestination.testAction('ecommerce', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + + it('should send Cart Updated event with minimal fields correctly', async () => { + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED, + action: undefined, + subtotal_value: undefined, + tax: undefined, + shipping: undefined + } + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + delete deepCopy.properties?.action + delete deepCopy.properties?.subtotal + delete deepCopy.properties?.tax + delete deepCopy.properties?.shipping + const e = createTestEvent(deepCopy) + delete e.properties?.product + + const json = { + events: [ + { + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + products: [ + { + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + quantity: 2, + price: 25, + metadata: { + color: 'red', + size: 'M' + } + }, + { + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: 'cart_id_1', + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' + } + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint).post('/users/track', json).reply(200) + + const response = await testDestination.testAction('ecommerce', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + it('should throw an error if missing identifier', async () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) @@ -517,12 +702,14 @@ describe('Braze.ecommerce', () => { const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy5: Partial = JSON.parse(JSON.stringify(payload)) const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.order_refunded' }) const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) const e4 = createTestEvent({ ...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) - const events = [e1, e2, e3, e4] + const e5 = createTestEvent({ ...deepCopy5, userId: 'userId5', event: 'ecommerce.cart_updated' }) + const events = [e1, e2, e3, e4, e5] const mapping2 = { ...mapping, @@ -573,12 +760,15 @@ describe('Braze.ecommerce', () => { ], total_value: 100, order_id: 'order_id_1', + cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, { code: 'VIPCUSTOMER', amount: 5 } - ], - cart_id: 'cart_id_1' + ] }, _update_existing_only: true }, @@ -674,7 +864,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: 'checkout_id_1', - cart_id: 'cart_id_1' + cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -721,6 +914,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: 'order_id_1', cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, @@ -728,6 +924,55 @@ describe('Braze.ecommerce', () => { ] }, _update_existing_only: true + }, + { + external_id: 'userId5', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' + }, + products: [ + { + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + quantity: 2, + price: 25, + metadata: { color: 'red', size: 'M' } + }, + { + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: 'cart_id_1', + action: 'replace', + subtotal_value: 85, + tax: 9, + shipping: 6 + }, + _update_existing_only: true } ] } @@ -859,7 +1104,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: 'checkout_id_1', - cart_id: 'cart_id_1' + cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -906,6 +1154,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: 'order_id_1', cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, @@ -925,268 +1176,24 @@ describe('Braze.ecommerce', () => { const responseJSON = [ { status: 200, - sent: { - name: 'ecommerce.order_refunded', - external_id: 'userId1', - user_alias: { - alias_name: 'alias_name_1', - alias_label: 'alias_label_1' - }, - email: 'email@email.com', - phone: '+14155551234', - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - time: '2024-06-10T12:00:00.000Z', - checkout_id: 'checkout_id_1', - order_id: 'order_id_1', - cart_id: 'cart_id_1', - total_value: 100, - total_discounts: 10, - discounts: [ - { - code: 'SUMMER21', - amount: 5 - }, - { - code: 'VIPCUSTOMER', - amount: 5 - } - ], - currency: 'USD', - source: 'test_source', - products: [ - { - product_id: 'prod_1', - product_name: 'Product 1', - variant_id: 'Size M', - image_url: 'https://example.com/prod1.jpg', - quantity: 2, - price: 25, - color: 'red', - size: 'M' - }, - { - product_id: 'prod_2', - product_name: 'Product 2', - variant_id: 'Size L', - image_url: 'https://example.com/prod2.jpg', - quantity: 1, - price: 50 - } - ], - metadata: { - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { - nested_key: 'nested_value' - }, - checkout_url: 'https://example.com/checkout', - order_status_url: 'https://example.com/order/status' - }, - enable_batching: true, - batch_size: 75, - index: 0 - }, - body: '{"external_id":"userId1","braze_id":"braze_id_1","email":"email@email.com","phone":"+14155551234","user_alias":{"alias_name":"alias_name_1","alias_label":"alias_label_1"},"app_id":"test_app_id","name":"ecommerce.order_refunded","time":"2024-06-10T12:00:00.000Z","properties":{"currency":"USD","source":"test_source","metadata":{"custom_field_1":"custom_value_1","custom_field_2":100,"custom_field_3":true,"custom_field_4":["a","b","c"],"custom_field_5":{"nested_key":"nested_value"},"checkout_url":"https://example.com/checkout","order_status_url":"https://example.com/order/status"},"products":[{"quantity":2,"product_id":"prod_1","product_name":"Product 1","variant_id":"Size M","price":25,"image_url":"https://example.com/prod1.jpg","metadata":{"color":"red","size":"M"}},{"quantity":1,"product_id":"prod_2","product_name":"Product 2","variant_id":"Size L","price":50,"image_url":"https://example.com/prod2.jpg"}],"total_value":100,"order_id":"order_id_1","total_discounts":10,"discounts":[{"code":"SUMMER21","amount":5},{"code":"VIPCUSTOMER","amount":5}]},"_update_existing_only":true}' + sent: json.events[0], + body: { success: true } }, { status: 400, errormessage: 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.', - sent: { - name: 'ecommerce.order_placed', - cancel_reason: "I didn't like it", - time: '2024-06-10T12:00:00.000Z', - checkout_id: 'checkout_id_1', - order_id: 'order_id_1', - cart_id: 'cart_id_1', - total_value: 100, - total_discounts: 10, - discounts: [ - { - code: 'SUMMER21', - amount: 5 - }, - { - code: 'VIPCUSTOMER', - amount: 5 - } - ], - currency: 'USD', - source: 'test_source', - products: [ - { - product_id: 'prod_1', - product_name: 'Product 1', - variant_id: 'Size M', - image_url: 'https://example.com/prod1.jpg', - quantity: 2, - price: 25, - color: 'red', - size: 'M' - }, - { - product_id: 'prod_2', - product_name: 'Product 2', - variant_id: 'Size L', - image_url: 'https://example.com/prod2.jpg', - quantity: 1, - price: 50 - } - ], - metadata: { - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { - nested_key: 'nested_value' - }, - checkout_url: 'https://example.com/checkout', - order_status_url: 'https://example.com/order/status' - }, - enable_batching: true, - batch_size: 75 - }, errortype: 'BAD_REQUEST', - errorreporter: 'DESTINATION' + errorreporter: 'INTEGRATIONS' }, { status: 200, - sent: { - name: 'ecommerce.checkout_started', - external_id: 'userId3', - user_alias: { - alias_name: 'alias_name_1', - alias_label: 'alias_label_1' - }, - email: 'email@email.com', - phone: '+14155551234', - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - time: '2024-06-10T12:00:00.000Z', - checkout_id: 'checkout_id_1', - order_id: 'order_id_1', - cart_id: 'cart_id_1', - total_value: 100, - total_discounts: 10, - discounts: [ - { - code: 'SUMMER21', - amount: 5 - }, - { - code: 'VIPCUSTOMER', - amount: 5 - } - ], - currency: 'USD', - source: 'test_source', - products: [ - { - product_id: 'prod_1', - product_name: 'Product 1', - variant_id: 'Size M', - image_url: 'https://example.com/prod1.jpg', - quantity: 2, - price: 25, - color: 'red', - size: 'M' - }, - { - product_id: 'prod_2', - product_name: 'Product 2', - variant_id: 'Size L', - image_url: 'https://example.com/prod2.jpg', - quantity: 1, - price: 50 - } - ], - metadata: { - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { - nested_key: 'nested_value' - }, - checkout_url: 'https://example.com/checkout', - order_status_url: 'https://example.com/order/status' - }, - enable_batching: true, - batch_size: 75, - index: 1 - }, - body: '{"external_id":"userId3","braze_id":"braze_id_1","email":"email@email.com","phone":"+14155551234","user_alias":{"alias_name":"alias_name_1","alias_label":"alias_label_1"},"app_id":"test_app_id","name":"ecommerce.checkout_started","time":"2024-06-10T12:00:00.000Z","properties":{"currency":"USD","source":"test_source","metadata":{"custom_field_1":"custom_value_1","custom_field_2":100,"custom_field_3":true,"custom_field_4":["a","b","c"],"custom_field_5":{"nested_key":"nested_value"},"checkout_url":"https://example.com/checkout","order_status_url":"https://example.com/order/status"},"products":[{"quantity":2,"product_id":"prod_1","product_name":"Product 1","variant_id":"Size M","price":25,"image_url":"https://example.com/prod1.jpg","metadata":{"color":"red","size":"M"}},{"quantity":1,"product_id":"prod_2","product_name":"Product 2","variant_id":"Size L","price":50,"image_url":"https://example.com/prod2.jpg"}],"total_value":100,"checkout_id":"checkout_id_1","cart_id":"cart_id_1"},"_update_existing_only":true}' + sent: json.events[1], + body: { success: true } }, { status: 200, - sent: { - name: 'ecommerce.order_cancelled', - external_id: 'userId4', - user_alias: { - alias_name: 'alias_name_1', - alias_label: 'alias_label_1' - }, - email: 'email@email.com', - phone: '+14155551234', - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - time: '2024-06-10T12:00:00.000Z', - checkout_id: 'checkout_id_1', - order_id: 'order_id_1', - cart_id: 'cart_id_1', - total_value: 100, - total_discounts: 10, - discounts: [ - { - code: 'SUMMER21', - amount: 5 - }, - { - code: 'VIPCUSTOMER', - amount: 5 - } - ], - currency: 'USD', - source: 'test_source', - products: [ - { - product_id: 'prod_1', - product_name: 'Product 1', - variant_id: 'Size M', - image_url: 'https://example.com/prod1.jpg', - quantity: 2, - price: 25, - color: 'red', - size: 'M' - }, - { - product_id: 'prod_2', - product_name: 'Product 2', - variant_id: 'Size L', - image_url: 'https://example.com/prod2.jpg', - quantity: 1, - price: 50 - } - ], - metadata: { - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { - nested_key: 'nested_value' - }, - checkout_url: 'https://example.com/checkout', - order_status_url: 'https://example.com/order/status' - }, - enable_batching: true, - batch_size: 75, - index: 2 - }, - body: '{"external_id":"userId4","braze_id":"braze_id_1","email":"email@email.com","phone":"+14155551234","user_alias":{"alias_name":"alias_name_1","alias_label":"alias_label_1"},"app_id":"test_app_id","name":"ecommerce.order_cancelled","time":"2024-06-10T12:00:00.000Z","properties":{"currency":"USD","source":"test_source","metadata":{"custom_field_1":"custom_value_1","custom_field_2":100,"custom_field_3":true,"custom_field_4":["a","b","c"],"custom_field_5":{"nested_key":"nested_value"},"checkout_url":"https://example.com/checkout","order_status_url":"https://example.com/order/status"},"products":[{"quantity":2,"product_id":"prod_1","product_name":"Product 1","variant_id":"Size M","price":25,"image_url":"https://example.com/prod1.jpg","metadata":{"color":"red","size":"M"}},{"quantity":1,"product_id":"prod_2","product_name":"Product 2","variant_id":"Size L","price":50,"image_url":"https://example.com/prod2.jpg"}],"total_value":100,"order_id":"order_id_1","cancel_reason":"I didn\'t like it","total_discounts":10,"discounts":[{"code":"SUMMER21","amount":5},{"code":"VIPCUSTOMER","amount":5}]},"_update_existing_only":true}' + sent: json.events[2], + body: { success: true } } ] @@ -1201,6 +1208,93 @@ describe('Braze.ecommerce', () => { expect(response).toEqual(responseJSON) }) + it('should attribute Braze errors to the correct payload when an earlier payload is filtered by validate()', async () => { + const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({ ...deepCopy1, event: 'ecommerce.order_refunded' }) + e1.userId = undefined + delete e1.properties?.email + delete e1.properties?.phone + delete e1.properties?.braze_id + delete e1.anonymousId + delete e1.properties?.user_alias + + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.checkout_started' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.order_cancelled' }) + + const events = [e1, e2, e3] + + let sentJson: any + nock(settings.endpoint) + .post('/users/track', (body) => { + sentJson = body + return true + }) + .reply(200, { errors: [{ index: 1, type: 'a valid identifier is required' }] }) + + const response = await testDestination.executeBatch('ecommerce', { + events, + settings, + mapping: { ...mapping, __segment_internal_sync_mode: 'update', name: { '@path': '$.event' } } + }) + + expect(response[0]).toEqual({ + status: 400, + errortype: 'BAD_REQUEST', + errorreporter: 'INTEGRATIONS', + errormessage: 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.' + }) + expect(response[1]).toEqual({ + status: 200, + sent: sentJson.events[0], + body: { success: true } + }) + expect(response[2]).toEqual({ + status: 400, + errortype: 'BAD_REQUEST', + errorreporter: 'DESTINATION', + errormessage: 'a valid identifier is required', + sent: sentJson.events[1], + body: 'a valid identifier is required' + }) + }) + + it('should populate sent and body correctly for a fully successful batch', async () => { + const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.checkout_started' }) + const events = [e1, e2] + + let sentJson: any + nock(settings.endpoint) + .post('/users/track', (body) => { + sentJson = body + return true + }) + .reply(200) + + const response = await testDestination.executeBatch('ecommerce', { + events, + settings, + mapping: { ...mapping, __segment_internal_sync_mode: 'add', name: { '@path': '$.event' } } + }) + + expect(response[0]).toEqual({ + status: 200, + sent: sentJson.events[0], + body: { success: true } + }) + expect(response[1]).toEqual({ + status: 200, + sent: sentJson.events[1], + body: { success: true } + }) + }) + it('should return correct multistatus response if there no SyncMode', async () => { const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) @@ -1317,7 +1411,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: 'checkout_id_1', - cart_id: 'cart_id_1' + cart_id: 'cart_id_1', + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -1364,6 +1461,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: 'order_id_1', cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: 'SUMMER21', amount: 5 }, @@ -1381,244 +1481,14 @@ describe('Braze.ecommerce', () => { name: { '@path': '$.event' } } - const responseJSON = [ - { - errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - errorreporter: 'DESTINATION', - errortype: 'BAD_REQUEST', - sent: { - batch_size: 75, - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - cart_id: 'cart_id_1', - checkout_id: 'checkout_id_1', - currency: 'USD', - discounts: [ - { amount: 5, code: 'SUMMER21' }, - { amount: 5, code: 'VIPCUSTOMER' } - ], - email: 'email@email.com', - enable_batching: true, - external_id: 'userId1', - metadata: { - checkout_url: 'https://example.com/checkout', - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { nested_key: 'nested_value' }, - order_status_url: 'https://example.com/order/status' - }, - name: 'ecommerce.order_refunded', - order_id: 'order_id_1', - phone: '+14155551234', - products: [ - { - color: 'red', - image_url: 'https://example.com/prod1.jpg', - price: 25, - product_id: 'prod_1', - product_name: 'Product 1', - quantity: 2, - size: 'M', - variant_id: 'Size M' - }, - { - image_url: 'https://example.com/prod2.jpg', - price: 50, - product_id: 'prod_2', - product_name: 'Product 2', - quantity: 1, - variant_id: 'Size L' - } - ], - source: 'test_source', - time: '2024-06-10T12:00:00.000Z', - total_discounts: 10, - total_value: 100, - user_alias: { - alias_label: 'alias_label_1', - alias_name: 'alias_name_1' - } - }, - status: 400 - }, - { - errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - errorreporter: 'DESTINATION', - errortype: 'BAD_REQUEST', - sent: { - batch_size: 75, - cancel_reason: "I didn't like it", - cart_id: 'cart_id_1', - checkout_id: 'checkout_id_1', - currency: 'USD', - discounts: [ - { amount: 5, code: 'SUMMER21' }, - { amount: 5, code: 'VIPCUSTOMER' } - ], - enable_batching: true, - metadata: { - checkout_url: 'https://example.com/checkout', - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { nested_key: 'nested_value' }, - order_status_url: 'https://example.com/order/status' - }, - name: 'ecommerce.order_placed', - order_id: 'order_id_1', - products: [ - { - color: 'red', - image_url: 'https://example.com/prod1.jpg', - price: 25, - product_id: 'prod_1', - product_name: 'Product 1', - quantity: 2, - size: 'M', - variant_id: 'Size M' - }, - { - image_url: 'https://example.com/prod2.jpg', - price: 50, - product_id: 'prod_2', - product_name: 'Product 2', - quantity: 1, - variant_id: 'Size L' - } - ], - source: 'test_source', - time: '2024-06-10T12:00:00.000Z', - total_discounts: 10, - total_value: 100 - }, - status: 400 - }, - { - errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - errorreporter: 'DESTINATION', - errortype: 'BAD_REQUEST', - sent: { - batch_size: 75, - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - cart_id: 'cart_id_1', - checkout_id: 'checkout_id_1', - currency: 'USD', - discounts: [ - { amount: 5, code: 'SUMMER21' }, - { amount: 5, code: 'VIPCUSTOMER' } - ], - email: 'email@email.com', - enable_batching: true, - external_id: 'userId3', - metadata: { - checkout_url: 'https://example.com/checkout', - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { nested_key: 'nested_value' }, - order_status_url: 'https://example.com/order/status' - }, - name: 'ecommerce.checkout_started', - order_id: 'order_id_1', - phone: '+14155551234', - products: [ - { - color: 'red', - image_url: 'https://example.com/prod1.jpg', - price: 25, - product_id: 'prod_1', - product_name: 'Product 1', - quantity: 2, - size: 'M', - variant_id: 'Size M' - }, - { - image_url: 'https://example.com/prod2.jpg', - price: 50, - product_id: 'prod_2', - product_name: 'Product 2', - quantity: 1, - variant_id: 'Size L' - } - ], - source: 'test_source', - time: '2024-06-10T12:00:00.000Z', - total_discounts: 10, - total_value: 100, - user_alias: { - alias_label: 'alias_label_1', - alias_name: 'alias_name_1' - } - }, - status: 400 - }, - { - errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - errorreporter: 'DESTINATION', - errortype: 'BAD_REQUEST', - sent: { - batch_size: 75, - braze_id: 'braze_id_1', - cancel_reason: "I didn't like it", - cart_id: 'cart_id_1', - checkout_id: 'checkout_id_1', - currency: 'USD', - discounts: [ - { amount: 5, code: 'SUMMER21' }, - { amount: 5, code: 'VIPCUSTOMER' } - ], - email: 'email@email.com', - enable_batching: true, - external_id: 'userId4', - metadata: { - checkout_url: 'https://example.com/checkout', - custom_field_1: 'custom_value_1', - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ['a', 'b', 'c'], - custom_field_5: { nested_key: 'nested_value' }, - order_status_url: 'https://example.com/order/status' - }, - name: 'ecommerce.order_cancelled', - order_id: 'order_id_1', - phone: '+14155551234', - products: [ - { - color: 'red', - image_url: 'https://example.com/prod1.jpg', - price: 25, - product_id: 'prod_1', - product_name: 'Product 1', - quantity: 2, - size: 'M', - variant_id: 'Size M' - }, - { - image_url: 'https://example.com/prod2.jpg', - price: 50, - product_id: 'prod_2', - product_name: 'Product 2', - quantity: 1, - variant_id: 'Size L' - } - ], - source: 'test_source', - time: '2024-06-10T12:00:00.000Z', - total_discounts: 10, - total_value: 100, - user_alias: { - alias_label: 'alias_label_1', - alias_name: 'alias_name_1' - } - }, - status: 400 - } - ] + const invalidSyncModeError = { + status: 400, + errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", + errortype: 'BAD_REQUEST', + errorreporter: 'INTEGRATIONS' + } + + const responseJSON = [invalidSyncModeError, invalidSyncModeError, invalidSyncModeError, invalidSyncModeError] nock(settings.endpoint).post('/users/track', json).reply(200) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts b/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts index 52599d1b666..ea60e06e9e0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts @@ -1,7 +1,7 @@ export const EVENT_NAMES = { PRODUCT_VIEWED: 'ecommerce.product_viewed', CHECKOUT_STARTED: 'ecommerce.checkout_started', - //CART_UPDATED: 'ecommerce.cart_updated', + CART_UPDATED: 'ecommerce.cart_updated', ORDER_PLACED: 'ecommerce.order_placed', ORDER_CANCELLED: 'ecommerce.order_cancelled', ORDER_REFUNDED: 'ecommerce.order_refunded' diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index 541969ebbf0..e3b1aa4d805 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -9,7 +9,7 @@ const name: InputField = { required: true, choices: [ { label: 'Product Viewed', value: EVENT_NAMES.PRODUCT_VIEWED }, - //{ label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED }, + { label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED }, { label: 'Checkout Started', value: EVENT_NAMES.CHECKOUT_STARTED }, { label: 'Order Placed', value: EVENT_NAMES.ORDER_PLACED }, { label: 'Order Cancelled', value: EVENT_NAMES.ORDER_CANCELLED }, @@ -198,27 +198,27 @@ const order_id: InputField = { const cart_id: InputField = { label: 'Cart ID', - description: 'Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping.', + description: 'Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping.', type: 'string', default: {'@path': '$.properties.cart_id'}, - // required: { - // match: 'any', - // conditions: [ - // { - // fieldKey: 'name', - // operator: 'is', - // value: [EVENT_NAMES.CART_UPDATED] - // } - // ] - // }, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + } + ] + }, depends_on: { match: 'any', conditions: [ - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', @@ -241,16 +241,16 @@ const total_value: InputField = { required: { match: 'any', conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', value: EVENT_NAMES.CHECKOUT_STARTED }, - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, { fieldKey: 'name', operator: 'is', @@ -271,16 +271,16 @@ const total_value: InputField = { depends_on: { match: 'any', conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', value: EVENT_NAMES.CHECKOUT_STARTED }, - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, { fieldKey: 'name', operator: 'is', @@ -300,6 +300,128 @@ const total_value: InputField = { } } +const action: InputField = { + label: 'Action', + description: 'The cart action that was performed (add, remove, or replace).', + type: 'string', + default: {'@path': '$.properties.action'}, + choices: [ + { label: 'Add', value: 'add' }, + { label: 'Remove', value: 'remove' }, + { label: 'Replace', value: 'replace' } + ], + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + } + ] + } +} + +const subtotal_value: InputField = { + label: 'Subtotal Value', + description: 'Subtotal monetary value of the cart before tax and shipping.', + type: 'number', + default: { '@path': '$.properties.subtotal'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } +} + +const tax: InputField = { + label: 'Tax', + description: 'Tax amount applied to the transaction.', + type: 'number', + default: { '@path': '$.properties.tax'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } +} + +const shipping: InputField = { + label: 'Shipping', + description: 'Shipping cost for the transaction.', + type: 'number', + default: { '@path': '$.properties.shipping'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } +} + const total_discounts: InputField = { label: 'Total Discounts', description: 'Total amount of discounts applied to the order.', @@ -527,6 +649,29 @@ const metadata: InputField = { defaultObjectUI: 'keyvalue' } +const catalog_type: InputField = { + label: 'Catalog Trigger Type', + description: 'Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock.', + type: 'string', + multiple: true, + choices: [ + { label: 'Price Drop', value: 'price_drop' }, + { label: 'Back In Stock', value: 'back_in_stock' } + ], + default: { '@path': '$.properties.type' }, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.PRODUCT_VIEWED + } + ] + } +} + const enable_batching: InputField = { type: 'boolean', label: 'Batch Data to Braze', @@ -558,6 +703,10 @@ export const commonFields = { order_id, cart_id, total_value, + action, + subtotal_value, + tax, + shipping, total_discounts, discounts, currency, @@ -565,4 +714,6 @@ export const commonFields = { metadata, enable_batching, batch_size -} \ No newline at end of file +} + +export { catalog_type } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index e2c1a56af06..fa0da4ffdc7 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -2,49 +2,48 @@ import { Payload } from './generated-types' import { Payload as SingleProductPayload } from '../ecommerceSingleProduct/generated-types' import { Settings } from '../generated-types' import { BrazeTrackUserAPIResponse } from '../utils' -import { - JSONLikeObject, - RequestClient, - PayloadValidationError, - MultiStatusResponse -} from '@segment/actions-core' +import { JSONLikeObject, RequestClient, PayloadValidationError, MultiStatusResponse } from '@segment/actions-core' import { SyncMode } from '@segment/actions-core/destination-kit/types' -import type { +import type { SupportedSyncMode, - BaseEvent, - EcommerceEvents, - EcommerceEvent, - MultiPropertyEventName, - ProductViewedEventName, + BaseEvent, + EcommerceEvents, + EcommerceEvent, + MultiPropertyEventName, + ProductViewedEventName, ProductViewedEvent, MultiProductBaseEvent, - //CartUpdatedEvent, + CartUpdatedEvent, CheckoutStartedEvent, OrderPlacedEvent, OrderRefundedEvent, OrderCancelledEvent, - PayloadWithIndex, + PayloadWithIndex, Product } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' -export async function send(request: RequestClient, payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean, syncMode?: SyncMode) { +export async function send( + request: RequestClient, + payloads: (Payload | SingleProductPayload)[], + settings: Settings, + isBatch: boolean, + syncMode?: SyncMode +) { const msResponse = new MultiStatusResponse() - if(!['update', 'add'].includes(syncMode ?? '')) { + if (!['update', 'add'].includes(syncMode ?? '')) { const message = `Invalid syncMode: ${syncMode}. Supported sync modes are 'add' and 'update'.` - if(isBatch){ - payloads.forEach((payload, index) => { + if (isBatch) { + payloads.forEach((_, index) => { msResponse.setErrorResponseAtIndex(index, { status: 400, - errormessage: message, - sent: payload as object as JSONLikeObject + errormessage: message }) }) return msResponse - } - else { + } else { throw new PayloadValidationError(message) } } @@ -55,54 +54,58 @@ export async function send(request: RequestClient, payloads: (Payload | SinglePr const response = await request(url, { method: 'POST', - ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), + ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), json }) - const errors = Array.isArray(response.data.errors) ? response.data.errors : [] + const errors = Array.isArray(response.data?.errors) ? response.data.errors : [] payloadsWithIndexes.forEach((payload, index) => { - - const error = errors.find(e => e.index === index) + const sentIndex = payload.index + if (sentIndex === undefined) { + return + } + + const error = errors.find((e) => e.index === sentIndex) - if(error){ + if (error) { msResponse.setErrorResponseAtIndex(index, { status: 400, errortype: 'BAD_REQUEST', errormessage: error.type, - sent: payload as object as JSONLikeObject, - body: JSON.stringify(json.events[index]) + sent: json.events[sentIndex] as object as JSONLikeObject, + body: error.type }) } }) - return isBatch ? msResponse : response + return isBatch ? msResponse : response } -function getJSON(payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean, syncMode: SupportedSyncMode, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { - const payloadsWithIndexes: PayloadWithIndex[] = [ ...payloads ] +function getJSON( + payloads: (Payload | SingleProductPayload)[], + settings: Settings, + isBatch: boolean, + syncMode: SupportedSyncMode, + msResponse: MultiStatusResponse +): { json: EcommerceEvents; payloadsWithIndexes: PayloadWithIndex[] } { + const payloadsWithIndexes: PayloadWithIndex[] = [...payloads] const events: EcommerceEvent[] = [] payloadsWithIndexes.forEach((payload, index) => { - const message = validate(payload, isBatch) - if(message) { + const message = validate(payload, isBatch) + if (message) { payload.index = undefined - msResponse.setErrorResponseAtIndex( - index, - { - status: 400, - errormessage: message, - sent: payload as object as JSONLikeObject - } - ) - } - else { - // assume valid payload - we'll overwrite later if Braze responds with an error for this index + msResponse.setErrorResponseAtIndex(index, { + status: 400, + errormessage: message + }) + } else { const event = getJSONItem(payload, settings, syncMode) payload.index = events.length events.push(event) msResponse.setSuccessResponseAtIndex(index, { status: 200, - sent: payload as object as JSONLikeObject, - body: JSON.stringify(event) + sent: event as object as JSONLikeObject, + body: { success: true } }) } }) @@ -110,11 +113,14 @@ function getJSON(payloads: (Payload | SingleProductPayload)[], settings: Setting return { json: { events }, payloadsWithIndexes } } -function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings, syncMode: SupportedSyncMode): EcommerceEvent { - +function getJSONItem( + payload: Payload | SingleProductPayload, + settings: Settings, + syncMode: SupportedSyncMode +): EcommerceEvent { const { app_id } = settings - const { + const { external_id, braze_id, email, @@ -130,129 +136,124 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings const time = dayjs(payloadTime).toISOString() const updateExistingOnly = (() => { - if ( user_alias?.alias_label && user_alias?.alias_name ) { - return true + if (user_alias?.alias_label && user_alias?.alias_name) { + return true } - if ( syncMode === 'update' ) { + if (syncMode === 'update') { return true } return undefined })() const baseEvent: BaseEvent = { - ...(external_id? { external_id } : {} ), - ...(braze_id? { braze_id } : {} ), - ...(email? { email } : {} ), - ...(phone? { phone } : {} ), - ...(user_alias? { user_alias } : {} ), - ...(app_id ? { app_id } : {} ), + ...(external_id ? { external_id } : {}), + ...(braze_id ? { braze_id } : {}), + ...(email ? { email } : {}), + ...(phone ? { phone } : {}), + ...(user_alias ? { user_alias } : {}), + ...(app_id ? { app_id } : {}), name: name as ProductViewedEventName | MultiPropertyEventName, time, properties: { currency, source, - ...(metadata ? { metadata } : {}) + ...(metadata ? { metadata } : {}) }, - ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {} ) + ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {}) } - switch(name) { + switch (name) { case EVENT_NAMES.PRODUCT_VIEWED: { - const { - product - } = payload as SingleProductPayload + const { product, catalog_type } = payload as SingleProductPayload const event: ProductViewedEvent = { ...baseEvent, name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...product + ...product, + ...(catalog_type && catalog_type.length > 0 ? { type: catalog_type } : {}) } } return event - } - // case EVENT_NAMES.CART_UPDATED: + } + case EVENT_NAMES.CART_UPDATED: case EVENT_NAMES.CHECKOUT_STARTED: case EVENT_NAMES.ORDER_PLACED: case EVENT_NAMES.ORDER_CANCELLED: case EVENT_NAMES.ORDER_REFUNDED: { - const { - total_value - } = payload as Payload + const { total_value } = payload as Payload const multiProductEvent: MultiProductBaseEvent = { ...baseEvent, name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products: getProductAndMetadataJSON( payload as Payload ), + products: getProductAndMetadataJSON(payload as Payload), total_value: total_value as number } } - switch(name) { - // case EVENT_NAMES.CART_UPDATED: { - // const { cart_id } = payload - - // const event: CartUpdatedEvent = { - // ...multiProductEvent, - // name: EVENT_NAMES.CART_UPDATED, - // properties: { - // ...multiProductEvent.properties, - // cart_id: cart_id as string - // } - // } - // return event - // } + switch (name) { + case EVENT_NAMES.CART_UPDATED: { + const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload + + const event: CartUpdatedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CART_UPDATED, + properties: { + ...multiProductEvent.properties, + cart_id: cart_id as string, + ...(action ? { action: action as 'add' | 'remove' | 'replace' } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}) + } + } + return event + } case EVENT_NAMES.CHECKOUT_STARTED: { - const { - checkout_id, - cart_id - } = payload - + const { checkout_id, cart_id, subtotal_value, tax, shipping } = payload as Payload + const event: CheckoutStartedEvent = { ...multiProductEvent, name: EVENT_NAMES.CHECKOUT_STARTED, properties: { ...multiProductEvent.properties, checkout_id: checkout_id as string, - ...(cart_id ? { cart_id } : {}) + ...(cart_id ? { cart_id } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}) } } return event } case EVENT_NAMES.ORDER_PLACED: { - const { - order_id, - cart_id, - total_discounts, - discounts - } = payload - + const { order_id, cart_id, subtotal_value, tax, shipping, total_discounts, discounts } = payload as Payload + const event: OrderPlacedEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_PLACED, properties: { ...multiProductEvent.properties, order_id: order_id as string, + ...(cart_id ? { cart_id } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(cart_id ? { cart_id } : {}) + ...(discounts ? { discounts } : {}) } } return event } case EVENT_NAMES.ORDER_REFUNDED: { - const { - order_id, - total_discounts, - discounts - } = payload - + const { order_id, total_discounts, discounts } = payload as Payload + const event: OrderRefundedEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_REFUNDED, @@ -267,13 +268,9 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.ORDER_CANCELLED: { - const { - order_id, - cancel_reason, - total_discounts, - discounts - } = payload - + const { order_id, cancel_reason, subtotal_value, tax, shipping, total_discounts, discounts } = + payload as Payload + const event: OrderCancelledEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_CANCELLED, @@ -281,6 +278,9 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings ...multiProductEvent.properties, order_id: order_id as string, cancel_reason: cancel_reason as string, + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), ...(discounts ? { discounts } : {}) } @@ -300,21 +300,12 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } function getProductAndMetadataJSON(payload: Payload): Product[] { - return payload.products.map(product => { - const { - quantity, - product_id, - product_name, - variant_id, - price, - image_url, - product_url, - ...metadata - } = product + return payload.products.map((product) => { + const { quantity, product_id, product_name, variant_id, price, image_url, product_url, ...metadata } = product return { quantity, - product_id, + product_id, product_name, variant_id, price, @@ -324,15 +315,14 @@ function getProductAndMetadataJSON(payload: Payload): Product[] { } }) } - + function validate(payload: Payload | SingleProductPayload, isBatch: boolean): string | void { const { braze_id, user_alias, external_id, email, phone } = payload if (!braze_id && !user_alias && !external_id && !email && !phone) { const message = 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.' - if(!isBatch) { - throw new PayloadValidationError(message) - } - else { + if (!isBatch) { + throw new PayloadValidationError(message) + } else { return message } } @@ -340,34 +330,278 @@ function validate(payload: Payload | SingleProductPayload, isBatch: boolean): st export function currencies() { const codes = [ - "AFN","EUR","ALL","DZD","USD","EUR","AOA","XCD","XCD","XAD","ARS","AMD", - "AWG","AUD","EUR","AZN","BSD","BHD","BDT","BBD","BYN","EUR","BZD","XOF", - "BMD","INR","BTN","BOB","BOV","USD","BAM","BWP","NOK","BRL","USD","BND", - "BGN","XOF","BIF","CVE","KHR","XAF","CAD","KYD","XAF","XAF","CLP","CLF", - "CNY","AUD","AUD","COP","COU","KMF","CDF","XAF","NZD","CRC","XOF","EUR", - "CUP","XCG","EUR","CZK","DKK","DJF","XCD","DOP","USD","EGP","SVC","USD", - "XAF","ERN","EUR","SZL","ETB","EUR","FKP","DKK","FJD","EUR","EUR","EUR", - "XPF","EUR","XAF","GMD","GEL","EUR","GHS","GIP","EUR","DKK","XCD","EUR", - "USD","GTQ","GBP","GNF","XOF","GYD","HTG","USD","AUD","EUR","HNL","HKD", - "HUF","ISK","INR","IDR","XDR","IRR","IQD","EUR","GBP","ILS","EUR","JMD", - "JPY","GBP","JOD","KZT","KES","AUD","KPW","KRW","KWD","KGS","LAK","EUR", - "LBP","LSL","ZAR","LRD","LYD","CHF","EUR","EUR","MOP","MGA","MWK","MYR", - "MVR","XOF","EUR","USD","EUR","MRU","MUR","EUR","XUA","MXN","MXV","USD", - "MDL","EUR","MNT","EUR","XCD","MAD","MZN","MMK","NAD","ZAR","AUD","NPR", - "EUR","XPF","NZD","NIO","XOF","NGN","NZD","AUD","MKD","USD","NOK","OMR", - "PKR","USD","PAB","USD","PGK","PYG","PEN","PHP","NZD","PLN","EUR","USD", - "QAR","EUR","RON","RUB","RWF","EUR","SHP","XCD","XCD","EUR","EUR","XCD", - "WST","EUR","STN","SAR","XOF","RSD","SCR","SLE","SGD","XCG","XSU","EUR", - "EUR","SBD","SOS","ZAR","SSP","EUR","LKR","SDG","SRD","NOK","SEK","CHF", - "CHE","CHW","SYP","TWD","TJS","TZS","THB","USD","XOF","NZD","TOP","TTD", - "TND","TRY","TMT","USD","AUD","UGX","UAH","AED","GBP","USD","USD","USN", - "UYU","UYI","UYW","UZS","VUV","VES","VED","VND","USD","USD","XPF","MAD", - "YER","ZMW","ZWG" + 'AFN', + 'EUR', + 'ALL', + 'DZD', + 'USD', + 'EUR', + 'AOA', + 'XCD', + 'XCD', + 'XAD', + 'ARS', + 'AMD', + 'AWG', + 'AUD', + 'EUR', + 'AZN', + 'BSD', + 'BHD', + 'BDT', + 'BBD', + 'BYN', + 'EUR', + 'BZD', + 'XOF', + 'BMD', + 'INR', + 'BTN', + 'BOB', + 'BOV', + 'USD', + 'BAM', + 'BWP', + 'NOK', + 'BRL', + 'USD', + 'BND', + 'BGN', + 'XOF', + 'BIF', + 'CVE', + 'KHR', + 'XAF', + 'CAD', + 'KYD', + 'XAF', + 'XAF', + 'CLP', + 'CLF', + 'CNY', + 'AUD', + 'AUD', + 'COP', + 'COU', + 'KMF', + 'CDF', + 'XAF', + 'NZD', + 'CRC', + 'XOF', + 'EUR', + 'CUP', + 'XCG', + 'EUR', + 'CZK', + 'DKK', + 'DJF', + 'XCD', + 'DOP', + 'USD', + 'EGP', + 'SVC', + 'USD', + 'XAF', + 'ERN', + 'EUR', + 'SZL', + 'ETB', + 'EUR', + 'FKP', + 'DKK', + 'FJD', + 'EUR', + 'EUR', + 'EUR', + 'XPF', + 'EUR', + 'XAF', + 'GMD', + 'GEL', + 'EUR', + 'GHS', + 'GIP', + 'EUR', + 'DKK', + 'XCD', + 'EUR', + 'USD', + 'GTQ', + 'GBP', + 'GNF', + 'XOF', + 'GYD', + 'HTG', + 'USD', + 'AUD', + 'EUR', + 'HNL', + 'HKD', + 'HUF', + 'ISK', + 'INR', + 'IDR', + 'XDR', + 'IRR', + 'IQD', + 'EUR', + 'GBP', + 'ILS', + 'EUR', + 'JMD', + 'JPY', + 'GBP', + 'JOD', + 'KZT', + 'KES', + 'AUD', + 'KPW', + 'KRW', + 'KWD', + 'KGS', + 'LAK', + 'EUR', + 'LBP', + 'LSL', + 'ZAR', + 'LRD', + 'LYD', + 'CHF', + 'EUR', + 'EUR', + 'MOP', + 'MGA', + 'MWK', + 'MYR', + 'MVR', + 'XOF', + 'EUR', + 'USD', + 'EUR', + 'MRU', + 'MUR', + 'EUR', + 'XUA', + 'MXN', + 'MXV', + 'USD', + 'MDL', + 'EUR', + 'MNT', + 'EUR', + 'XCD', + 'MAD', + 'MZN', + 'MMK', + 'NAD', + 'ZAR', + 'AUD', + 'NPR', + 'EUR', + 'XPF', + 'NZD', + 'NIO', + 'XOF', + 'NGN', + 'NZD', + 'AUD', + 'MKD', + 'USD', + 'NOK', + 'OMR', + 'PKR', + 'USD', + 'PAB', + 'USD', + 'PGK', + 'PYG', + 'PEN', + 'PHP', + 'NZD', + 'PLN', + 'EUR', + 'USD', + 'QAR', + 'EUR', + 'RON', + 'RUB', + 'RWF', + 'EUR', + 'SHP', + 'XCD', + 'XCD', + 'EUR', + 'EUR', + 'XCD', + 'WST', + 'EUR', + 'STN', + 'SAR', + 'XOF', + 'RSD', + 'SCR', + 'SLE', + 'SGD', + 'XCG', + 'XSU', + 'EUR', + 'EUR', + 'SBD', + 'SOS', + 'ZAR', + 'SSP', + 'EUR', + 'LKR', + 'SDG', + 'SRD', + 'NOK', + 'SEK', + 'CHF', + 'CHE', + 'CHW', + 'SYP', + 'TWD', + 'TJS', + 'TZS', + 'THB', + 'USD', + 'XOF', + 'NZD', + 'TOP', + 'TTD', + 'TND', + 'TRY', + 'TMT', + 'USD', + 'AUD', + 'UGX', + 'UAH', + 'AED', + 'GBP', + 'USD', + 'USD', + 'USN', + 'UYU', + 'UYI', + 'UYW', + 'UZS', + 'VUV', + 'VES', + 'VED', + 'VND', + 'USD', + 'USD', + 'XPF', + 'MAD', + 'YER', + 'ZMW', + 'ZWG' ] const unique = Array.from(new Set(codes)) - return unique.map(code => ({ + return unique.map((code) => ({ label: code, value: code })) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts index 29e29851bdc..d19a8f0854b 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts @@ -45,13 +45,29 @@ export interface Payload { */ order_id?: string /** - * Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping. + * Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping. */ cart_id?: string /** * Total monetary value of the cart. */ total_value?: number + /** + * The cart action that was performed (add, remove, or replace). + */ + action?: string + /** + * Subtotal monetary value of the cart before tax and shipping. + */ + subtotal_value?: number + /** + * Tax amount applied to the transaction. + */ + tax?: number + /** + * Shipping cost for the transaction. + */ + shipping?: number /** * Total amount of discounts applied to the order. */ diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts index b7882dc20d0..2b17ec687f2 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -10,14 +10,14 @@ export type PayloadWithIndex = (Payload | SingleProductPayload) & { export type ProductViewedEventName = typeof EVENT_NAMES.PRODUCT_VIEWED export type CheckoutStartedEventName = typeof EVENT_NAMES.CHECKOUT_STARTED -//export type CartUpdatedEventName = typeof EVENT_NAMES.CART_UPDATED +export type CartUpdatedEventName = typeof EVENT_NAMES.CART_UPDATED export type OrderPlacedEventName = typeof EVENT_NAMES.ORDER_PLACED export type OrderCancelledEventName = typeof EVENT_NAMES.ORDER_CANCELLED export type OrderRefundedEventName = typeof EVENT_NAMES.ORDER_REFUNDED export type MultiPropertyEventName = CheckoutStartedEventName | - //CartUpdatedEventName | + CartUpdatedEventName | OrderPlacedEventName | OrderCancelledEventName | OrderRefundedEventName @@ -28,7 +28,7 @@ export interface EcommerceEvents { export type EcommerceEvent = | ProductViewedEvent - // | CartUpdatedEvent + | CartUpdatedEvent | CheckoutStartedEvent | OrderPlacedEvent | OrderRefundedEvent @@ -58,7 +58,9 @@ export interface BaseEvent { export interface ProductViewedEvent extends BaseEvent { name: ProductViewedEventName - properties: BaseEvent['properties'] & BaseProduct + properties: BaseEvent['properties'] & BaseProduct & { + type?: string[] + } } export interface MultiProductBaseEvent extends BaseEvent { @@ -85,21 +87,28 @@ export interface BaseProduct { product_url?: string } -// export interface CartUpdatedEvent extends MultiProductBaseEvent { -// name: CartUpdatedEventName -// properties: MultiProductBaseEvent['properties'] & { -// cart_id: string -// } -// } +export interface CartUpdatedEvent extends MultiProductBaseEvent { + name: CartUpdatedEventName + properties: MultiProductBaseEvent['properties'] & { + cart_id: string + action?: 'add' | 'remove' | 'replace' + subtotal_value?: number + tax?: number + shipping?: number + } +} export interface CheckoutStartedEvent extends MultiProductBaseEvent { name: CheckoutStartedEventName properties: MultiProductBaseEvent['properties'] & { - checkout_id: string - cart_id?: string - metadata?: BaseEvent['properties']['metadata'] & { + checkout_id: string + cart_id?: string + subtotal_value?: number + tax?: number + shipping?: number + metadata?: BaseEvent['properties']['metadata'] & { checkout_url?: string - } + } } } @@ -108,6 +117,9 @@ export interface OrderPlacedEvent extends MultiProductBaseEvent { properties: MultiProductBaseEvent['properties'] & { order_id: string cart_id?: string + subtotal_value?: number + tax?: number + shipping?: number total_discounts?: number discounts?: Array<{ code: string @@ -138,6 +150,9 @@ export interface OrderCancelledEvent extends MultiProductBaseEvent { name: OrderCancelledEventName properties: MultiProductBaseEvent['properties'] & { order_id: string + subtotal_value?: number + tax?: number + shipping?: number cancel_reason: string total_discounts?: number discounts?: Array<{ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap index 4b214ce5505..186cf114a61 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap @@ -22,6 +22,9 @@ Object { "product_name": "Q#t9o", "product_url": "http://egu.ci/ritpaz", "source": "Q#t9o", + "type": Array [ + "price_drop", + ], "variant_id": "Q#t9o", }, "time": Any, diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts index 675efc8bea0..f1464c08b69 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts @@ -151,6 +151,72 @@ describe('Braze.ecommerce', () => { expect(response.length).toBe(1) }) + + it('should send Product Viewed event with catalog_type correctly', async () => { + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + deepCopy.properties = { + ...deepCopy.properties, + type: ['price_drop', 'back_in_stock'] + } + const e = createTestEvent(deepCopy) + + const mapping2 = { + ...mapping, + catalog_type: { '@path': '$.properties.type' } + } + + const json = { + events: [ + { + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { + nested_key: 'nested_value' + }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' + }, + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + price: 25, + type: ['price_drop', 'back_in_stock'] + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint).post('/users/track', json).reply(200) + + const response = await testDestination.testAction('ecommerceSingleProduct', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) }) describe('batch events', () => { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts index 1bf9f69d061..8501e7c43ad 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts @@ -45,13 +45,29 @@ export interface Payload { */ order_id?: string /** - * Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping. + * Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping. */ cart_id?: string /** * Total monetary value of the cart. */ total_value?: number + /** + * The cart action that was performed (add, remove, or replace). + */ + action?: string + /** + * Subtotal monetary value of the cart before tax and shipping. + */ + subtotal_value?: number + /** + * Tax amount applied to the transaction. + */ + tax?: number + /** + * Shipping cost for the transaction. + */ + shipping?: number /** * Total amount of discounts applied to the order. */ @@ -85,6 +101,10 @@ export interface Payload { * Maximum number of events to include in each batch. Actual batch sizes may be lower. */ batch_size: number + /** + * Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock. + */ + catalog_type?: string[] /** * Product details associated with the ecommerce event. */ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts index d9c5692a859..41b317534d0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts @@ -1,9 +1,10 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { +import { commonFields, product, + catalog_type, } from '../ecommerce/fields' import { send } from '../ecommerce/functions' @@ -12,6 +13,7 @@ const action: ActionDefinition = { description: '(Beta) Send a single product [Ecommerce Recommended event](https://www.braze.com/docs/user_guide/data/activation/custom_data/recommended_events/ecommerce_events) to Braze.', fields: { ...commonFields, + catalog_type, product }, syncMode: { diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 85b600ee994..47f537f580e 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -101,7 +101,8 @@ const destination: DestinationDefinition = { 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" and event != "Product Added" and event != "Product Removed"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields), type: 'automatic' @@ -110,9 +111,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 +124,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 +137,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 +150,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,12 +163,56 @@ const destination: DestinationDefinition = { name: 'Product Viewed (beta)', subscribe: 'event = "Product Viewed"', partnerAction: 'ecommerceSingleProduct', - mapping: { + mapping: { ...defaultValues(ecommerceSingleProduct.fields), name: EVENT_NAMES.PRODUCT_VIEWED }, type: 'automatic' }, + { + name: 'Product Added (beta)', + subscribe: 'event = "Product Added"', + partnerAction: 'ecommerce', + mapping: { + ...defaultValues(ecommerce.fields), + name: EVENT_NAMES.CART_UPDATED, + action: 'add', + products: [ + { + product_id: { '@path': '$.properties.product_id' }, + product_name: { '@path': '$.properties.name' }, + variant_id: { '@path': '$.properties.variant' }, + image_url: { '@path': '$.properties.image_url' }, + product_url: { '@path': '$.properties.url' }, + quantity: { '@path': '$.properties.quantity' }, + price: { '@path': '$.properties.price' } + } + ] + }, + type: 'automatic' + }, + { + name: 'Product Removed (beta)', + subscribe: 'event = "Product Removed"', + partnerAction: 'ecommerce', + mapping: { + ...defaultValues(ecommerce.fields), + name: EVENT_NAMES.CART_UPDATED, + action: 'remove', + products: [ + { + product_id: { '@path': '$.properties.product_id' }, + product_name: { '@path': '$.properties.name' }, + variant_id: { '@path': '$.properties.variant' }, + image_url: { '@path': '$.properties.image_url' }, + product_url: { '@path': '$.properties.url' }, + quantity: { '@path': '$.properties.quantity' }, + price: { '@path': '$.properties.price' } + } + ] + }, + type: 'automatic' + }, { name: 'Identify Calls', subscribe: 'type = "identify"',