From ca74a8367db455429edf16d4a9c579056a6de52c Mon Sep 17 00:00:00 2001 From: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com> Date: Fri, 22 May 2026 16:42:25 +0300 Subject: [PATCH] fix(Pipedrive Node): Format date-only fields as YYYY-MM-DD (#30891) Co-authored-by: Claude Opus 4.7 Co-authored-by: Dawid Myslak --- .../Pipedrive/test/v2/customFields.test.ts | 33 ++++- .../test/v2/dateNormalisation.test.ts | 124 ++++++++++++++++++ .../test/v2/node/activity/create.test.ts | 1 + .../v2/node/activity/create.workflow.json | 3 +- .../test/v2/node/activity/update.test.ts | 1 + .../v2/node/activity/update.workflow.json | 3 +- .../test/v2/node/deal/createWithDate.test.ts | 66 ++++++++++ .../v2/node/deal/createWithDate.workflow.json | 101 ++++++++++++++ .../test/v2/node/deal/update.test.ts | 3 +- .../test/v2/node/deal/update.workflow.json | 5 +- .../v2/actions/activity/create.operation.ts | 4 +- .../v2/actions/activity/update.operation.ts | 4 +- .../v2/actions/deal/create.operation.ts | 4 +- .../v2/actions/deal/update.operation.ts | 4 +- .../nodes/Pipedrive/v2/helpers/index.ts | 2 +- .../Pipedrive/v2/helpers/typeCoercion.ts | 21 +++ 16 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 packages/nodes-base/nodes/Pipedrive/test/v2/dateNormalisation.test.ts create mode 100644 packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.test.ts create mode 100644 packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.workflow.json diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/customFields.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/customFields.test.ts index 4e1aad17c61..ef658834c97 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/customFields.test.ts +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/customFields.test.ts @@ -2,7 +2,12 @@ import type { IDataObject, INodeExecutionData } from 'n8n-workflow'; import { encodeCustomFieldsV2, resolveCustomFieldsV2 } from '../../v2/helpers/customFields'; import { addFieldsToBody } from '../../v2/helpers/fields'; -import { coerceToBoolean, coerceToNumber, toRfc3339 } from '../../v2/helpers/typeCoercion'; +import { + coerceToBoolean, + coerceToNumber, + toDateOnly, + toRfc3339, +} from '../../v2/helpers/typeCoercion'; import type { ICustomProperties } from '../../v2/transport'; const customProperties: ICustomProperties = { @@ -388,4 +393,30 @@ describe('Pipedrive v2 Type Coercion', () => { expect(toRfc3339('2024-01-15')).toBe('2024-01-15'); }); }); + + describe('toDateOnly', () => { + it('should pass through YYYY-MM-DD unchanged', () => { + expect(toDateOnly('2024-01-15')).toBe('2024-01-15'); + }); + it('should strip the time component from an ISO datetime', () => { + expect(toDateOnly('2024-01-15T00:00:00.000Z')).toBe('2024-01-15'); + expect(toDateOnly('2024-01-15T14:30:00Z')).toBe('2024-01-15'); + }); + it('should strip the time component from a space-separated datetime', () => { + expect(toDateOnly('2024-01-15 14:30:00')).toBe('2024-01-15'); + }); + it('should return an empty input unchanged', () => { + expect(toDateOnly('')).toBe(''); + }); + it('should return the original value when it cannot be parsed', () => { + expect(toDateOnly('not-a-date')).toBe('not-a-date'); + }); + it('should return non-standard inputs verbatim instead of shifting the calendar day through UTC', () => { + // Parsing 'Jan 15, 2024' as a local-time Date and converting via + // toISOString() can yield 2024-01-14 in timezones east of UTC. + // We deliberately avoid that conversion, so the input is passed + // through and Pipedrive surfaces the real validation error. + expect(toDateOnly('Jan 15, 2024')).toBe('Jan 15, 2024'); + }); + }); }); diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/dateNormalisation.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/dateNormalisation.test.ts new file mode 100644 index 00000000000..063805e344b --- /dev/null +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/dateNormalisation.test.ts @@ -0,0 +1,124 @@ +import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; + +import { execute as activityCreateExecute } from '../../v2/actions/activity/create.operation'; +import { execute as activityUpdateExecute } from '../../v2/actions/activity/update.operation'; +import { execute as dealCreateExecute } from '../../v2/actions/deal/create.operation'; +import { execute as dealUpdateExecute } from '../../v2/actions/deal/update.operation'; +import { pipedriveApiRequest, pipedriveGetCustomProperties } from '../../v2/transport'; + +jest.mock('../../v2/transport', () => ({ + pipedriveApiRequest: { call: jest.fn() }, + pipedriveApiRequestAllItemsCursor: { call: jest.fn() }, + pipedriveApiRequestAllItemsOffset: { call: jest.fn() }, + pipedriveGetCustomProperties: { call: jest.fn() }, +})); + +const mockApiRequest = pipedriveApiRequest as unknown as { call: jest.Mock }; +const mockGetCustomProperties = pipedriveGetCustomProperties as unknown as { call: jest.Mock }; + +function buildContext(params: Record): IExecuteFunctions { + return { + getInputData: jest.fn(() => [{ json: {} }]), + getNodeParameter: jest.fn((name: string, _i?: number, defaultValue?: unknown) => { + if (Object.prototype.hasOwnProperty.call(params, name)) return params[name]; + return defaultValue; + }), + continueOnFail: jest.fn(() => false), + helpers: { + returnJsonArray: jest.fn((data: unknown) => (Array.isArray(data) ? data : [data])), + constructExecutionMetaData: jest.fn((items: unknown) => items), + }, + getNode: jest.fn(() => ({})), + } as unknown as IExecuteFunctions; +} + +describe('Pipedrive v2 normalises ISO date inputs to YYYY-MM-DD on outgoing requests', () => { + beforeEach(() => { + mockApiRequest.call.mockReset().mockResolvedValue({ data: {}, additionalData: {} }); + mockGetCustomProperties.call.mockReset().mockResolvedValue({}); + }); + + it('deal/create strips the time component from expected_close_date', async () => { + const ctx = buildContext({ + rawCustomFieldKeys: true, + title: 'Test Deal', + associateWith: 'organization', + org_id: 7, + additionalFields: { expected_close_date: '2026-04-13T00:00:00.000Z' }, + }); + + await dealCreateExecute.call(ctx); + + const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [ + unknown, + string, + string, + IDataObject, + ]; + expect(method).toBe('POST'); + expect(endpoint).toBe('/deals'); + expect(body.expected_close_date).toBe('2026-04-13'); + }); + + it('deal/update strips the time component from expected_close_date', async () => { + const ctx = buildContext({ + rawCustomFieldKeys: true, + dealId: 10, + updateFields: { expected_close_date: '2026-04-13T00:00:00.000Z' }, + }); + + await dealUpdateExecute.call(ctx); + + const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [ + unknown, + string, + string, + IDataObject, + ]; + expect(method).toBe('PATCH'); + expect(endpoint).toBe('/deals/10'); + expect(body.expected_close_date).toBe('2026-04-13'); + }); + + it('activity/create strips the time component from due_date', async () => { + const ctx = buildContext({ + rawCustomFieldKeys: true, + subject: 'Call client', + done: false, + type: 'call', + additionalFields: { due_date: '2026-04-01T00:00:00.000Z' }, + }); + + await activityCreateExecute.call(ctx); + + const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [ + unknown, + string, + string, + IDataObject, + ]; + expect(method).toBe('POST'); + expect(endpoint).toBe('/activities'); + expect(body.due_date).toBe('2026-04-01'); + }); + + it('activity/update strips the time component from due_date', async () => { + const ctx = buildContext({ + rawCustomFieldKeys: true, + activityId: 10, + updateFields: { due_date: '2026-04-02T00:00:00.000Z' }, + }); + + await activityUpdateExecute.call(ctx); + + const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [ + unknown, + string, + string, + IDataObject, + ]; + expect(method).toBe('PATCH'); + expect(endpoint).toBe('/activities/10'); + expect(body.due_date).toBe('2026-04-02'); + }); +}); diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.test.ts index 11f9eb505b3..a7a0d90413f 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.test.ts +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.test.ts @@ -15,6 +15,7 @@ describe('Test PipedriveV2, activity => create', () => { deal_id: 8, person_id: 10, org_id: 7, + due_date: '2026-04-01', }) .reply(200, { success: true, diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.workflow.json b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.workflow.json index 3458947aff0..6f1d9f4e012 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.workflow.json +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/create.workflow.json @@ -20,7 +20,8 @@ "additionalFields": { "deal_id": 8, "person_id": 10, - "org_id": 7 + "org_id": 7, + "due_date": "2026-04-01T00:00:00.000Z" } }, "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.test.ts index 11da2c6e345..d1b74e8018c 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.test.ts +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.test.ts @@ -11,6 +11,7 @@ describe('Test PipedriveV2, activity => update', () => { .patch('/activities/10', { subject: 'Updated call', done: true, + due_date: '2026-04-02', }) .reply(200, { success: true, diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.workflow.json b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.workflow.json index 0a0bf570570..76d4539e530 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.workflow.json +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/activity/update.workflow.json @@ -17,7 +17,8 @@ "activityId": 10, "updateFields": { "subject": "Updated call", - "done": true + "done": true, + "due_date": "2026-04-02T00:00:00.000Z" } }, "id": "22222222-3333-4444-5555-666666666666", diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.test.ts new file mode 100644 index 00000000000..4993167f044 --- /dev/null +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.test.ts @@ -0,0 +1,66 @@ +import { NodeTestHarness } from '@nodes-testing/node-test-harness'; +import nock from 'nock'; + +import { credentials } from '../credentials'; +import { mockFieldsApi } from '../fieldsApiMock'; + +describe('Test PipedriveV2, deal => create (date-only normalisation)', () => { + mockFieldsApi('deal'); + + nock('https://api.pipedrive.com/api/v2') + .post('/deals', { + title: 'Test Deal', + org_id: 7, + expected_close_date: '2026-04-13', + }) + .reply(200, { + success: true, + data: { + id: 9, + title: 'Test Deal', + creator_user_id: 25455458, + value: null, + person_id: null, + org_id: 7, + stage_id: 6, + currency: 'USD', + add_time: '2026-04-01T22:03:27Z', + update_time: null, + status: 'open', + probability: null, + lost_reason: null, + visible_to: 3, + close_time: null, + pipeline_id: 2, + won_time: null, + lost_time: null, + stage_change_time: null, + local_won_date: null, + local_lost_date: null, + local_close_date: null, + expected_close_date: '2026-04-13', + custom_fields: { + test_string: null, + test_enum: null, + test_multi: null, + }, + owner_id: 25455458, + label_ids: [], + is_deleted: false, + origin: 'API', + origin_id: null, + channel: null, + channel_id: null, + acv: null, + arr: null, + mrr: null, + is_archived: false, + archive_time: null, + }, + }); + + new NodeTestHarness().setupTests({ + credentials, + workflowFiles: ['createWithDate.workflow.json'], + }); +}); diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.workflow.json b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.workflow.json new file mode 100644 index 00000000000..e74fc46657d --- /dev/null +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/createWithDate.workflow.json @@ -0,0 +1,101 @@ +{ + "name": "Pipedrive v2 deal create with date test", + "nodes": [ + { + "parameters": {}, + "id": "aa111111-bb22-cc33-dd44-ee5555555555", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "authentication": "apiToken", + "resource": "deal", + "operation": "create", + "title": "Test Deal", + "associateWith": "organization", + "org_id": 7, + "additionalFields": { + "expected_close_date": "2026-04-13T00:00:00.000Z" + } + }, + "id": "bb222222-cc33-dd44-ee55-ff6666666666", + "name": "Pipedrive", + "type": "n8n-nodes-base.pipedrive", + "typeVersion": 2, + "position": [200, 0], + "credentials": { + "pipedriveApi": { + "id": "cred-1", + "name": "Pipedrive API" + } + } + }, + { + "parameters": {}, + "id": "cc333333-dd44-ee55-ff66-aa7777777777", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [400, 0] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": 9, + "title": "Test Deal", + "creator_user_id": 25455458, + "value": null, + "person_id": null, + "org_id": 7, + "stage_id": 6, + "currency": "USD", + "add_time": "2026-04-01T22:03:27Z", + "update_time": null, + "status": "open", + "probability": null, + "lost_reason": null, + "visible_to": 3, + "close_time": null, + "pipeline_id": 2, + "won_time": null, + "lost_time": null, + "stage_change_time": null, + "local_won_date": null, + "local_lost_date": null, + "local_close_date": null, + "expected_close_date": "2026-04-13", + "custom_fields": { + "test_string": null, + "test_enum": null, + "test_multi": null + }, + "owner_id": 25455458, + "label_ids": [], + "is_deleted": false, + "origin": "API", + "origin_id": null, + "channel": null, + "channel_id": null, + "acv": null, + "arr": null, + "mrr": null, + "is_archived": false, + "archive_time": null + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [[{ "node": "Pipedrive", "type": "main", "index": 0 }]] + }, + "Pipedrive": { + "main": [[{ "node": "No Operation, do nothing", "type": "main", "index": 0 }]] + } + } +} diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.test.ts b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.test.ts index ded51f7924f..1db3626160c 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.test.ts +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.test.ts @@ -11,6 +11,7 @@ describe('Test PipedriveV2, deal => update', () => { .patch('/deals/10', { title: 'Updated Deal', value: 7500, + expected_close_date: '2026-04-13', }) .reply(200, { success: true, @@ -37,7 +38,7 @@ describe('Test PipedriveV2, deal => update', () => { local_won_date: null, local_lost_date: null, local_close_date: null, - expected_close_date: null, + expected_close_date: '2026-04-13', custom_fields: { f5ed368466cf0477371c6ee076252f49a188848e: null, '48233ee3e9d505bbcca8e7e05d9b8df4231021bc': null, diff --git a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.workflow.json b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.workflow.json index 0ee0ec2f7f2..d3b00679896 100644 --- a/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.workflow.json +++ b/packages/nodes-base/nodes/Pipedrive/test/v2/node/deal/update.workflow.json @@ -17,7 +17,8 @@ "dealId": 10, "updateFields": { "title": "Updated Deal", - "value": 7500 + "value": 7500, + "expected_close_date": "2026-04-13T00:00:00.000Z" } }, "id": "b8b8b8b8-1111-2222-3333-444444444444", @@ -67,7 +68,7 @@ "local_won_date": null, "local_lost_date": null, "local_close_date": null, - "expected_close_date": null, + "expected_close_date": "2026-04-13", "custom_fields": { "test_string": null, "test_enum": null, diff --git a/packages/nodes-base/nodes/Pipedrive/v2/actions/activity/create.operation.ts b/packages/nodes-base/nodes/Pipedrive/v2/actions/activity/create.operation.ts index 1e186a8ee57..bd486e4e84b 100644 --- a/packages/nodes-base/nodes/Pipedrive/v2/actions/activity/create.operation.ts +++ b/packages/nodes-base/nodes/Pipedrive/v2/actions/activity/create.operation.ts @@ -11,7 +11,7 @@ import { encodeCustomFieldsV2, resolveCustomFieldsV2, coerceToBoolean, - toRfc3339, + toDateOnly, addFieldsToBody, } from '../../helpers'; import { customFieldsCollection, rawCustomFieldKeysOption } from '../common.description'; @@ -138,7 +138,7 @@ export async function execute(this: IExecuteFunctions): Promise