diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json b/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json index 80683917d28..3bd81d893db 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json +++ b/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json @@ -1843,7 +1843,7 @@ "price": { "label": "Item Price", "description": "The price of the item, after discounts.", - "type": "integer", + "type": "number", "required": false, "multiple": false, "allowNull": false, diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts index cf201f941b3..a66b4da0aa5 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts @@ -210,6 +210,34 @@ describe('Microsoft Bing CAPI (Actions) - sendEvent (updated)', () => { expect(scope.isDone()).toBe(true) }) + test('decimal item price is accepted and forwarded (not rejected as non-integer)', async () => { + const event = buildTrackEvent() + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`, (body: any) => { + const items = body.data[0].customData.items + expect(items).toHaveLength(1) + expect(items[0].price).toBe(9.99) + expect(items[0].quantity).toBe(2) + expect(items[0].id).toBe('sku-1') + return true + }) + .reply(200, {}) + const responses: any = await testDestination.testAction('sendEvent', { + event, + settings, + mapping: { + data: { eventType: 'custom', eventTime: new Date('2024-01-01T00:00:00.000Z').toISOString() }, + customData: { value: 9.99 }, + items: [{ id: 'sku-1', name: 'Widget', price: 9.99, quantity: 2 }], + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + // testAction resolves (no mapping-kit validation error thrown) for a decimal price + expect(responses[0].status).toBe(200) + expect(scope.isDone()).toBe(true) + }) + test('pageLoad event requires page context mapping and is sent correctly', async () => { const iso = '2024-06-01T12:00:00.000Z' const event = buildTrackEvent({ type: 'page', event: undefined }) @@ -392,6 +420,108 @@ describe('Microsoft Bing CAPI (Actions) - sendEvent (updated)', () => { expect(scope.isDone()).toBe(true) }) + test('batch: warning-only detail (isWarning:true) at an index is treated as SUCCESS, not failure', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + // Microsoft returns HTTP 200 and, because continueOnValidationError:true, reports + // non-fatal issues as warnings while still accepting the event. + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { + eventsReceived: 2, + error: { + details: [ + { + errorCode: 'Empty', + errorMessage: "'price' must not be empty.", + index: 0, + isWarning: true, + propertyName: 'data[0].customData.items[0].price' + } + ] + } + }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + // Warning-only event was accepted by Microsoft -> must be a success, not a 400. + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(200) + expect(scope.isDone()).toBe(true) + }) + + test('batch: real error (isWarning:false) is still marked 400 while warning at another index stays 200', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { + eventsReceived: 1, + error: { + details: [ + { + errorCode: 'Empty', + errorMessage: "'price' must not be empty.", + index: 0, + isWarning: true, + propertyName: 'data[0].customData.items[0].price' + }, + { + errorCode: 'Invalid', + errorMessage: 'Second failed', + index: 1, + isWarning: false, + propertyName: 'data[1].eventName' + } + ] + } + }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + // index 0 had only a warning -> success; index 1 had a real error -> failure. + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(400) + expect(responses[1].errormessage).toContain('Second failed') + expect(scope.isDone()).toBe(true) + }) + + test('batch: error detail without isWarning field defaults to a real failure (400)', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { error: { details: [{ index: 1, errorMessage: 'Second failed' }] } }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(400) + expect(responses[1].errormessage).toContain('Second failed') + expect(scope.isDone()).toBe(true) + }) + test('phone digits normalized before hashing', async () => { const rawPhone = '+1 (555) 123-4567 ext.89' const event = buildTrackEvent({ context: { traits: { phone: rawPhone, email: 'norm@example.com' } } }) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts index 2b1309a1072..c55b52a7cd3 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts @@ -196,7 +196,7 @@ export const items: InputField = { price: { label: 'Item Price', description: 'The price of the item, after discounts.', - type: 'integer' + type: 'number' }, quantity: { label: 'Item Quantity', diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts index 6c1bd66b51a..48279b305b4 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts @@ -72,7 +72,7 @@ async function send(request: RequestClient, payloads: Payload[], settings: Setti const details = response.data?.error?.details ?? [] payloads.forEach((payload, index) => { - const error = details.find((detail) => detail.index === index) + const error = details.find((detail) => detail.index === index && !detail.isWarning) if (error) { multiStatusResponse.setErrorResponseAtIndex(index, { status: 400, diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts index dbd69b456ab..59690b29ef3 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts @@ -61,6 +61,8 @@ export interface MSMultiStatusResponse { propertyName: string attemptedValue: unknown errorMessage: string + errorCode?: string + isWarning?: boolean }> } traceId: string