diff --git a/packages/twenty-apps/internal/twenty-partners/package.json b/packages/twenty-apps/internal/twenty-partners/package.json index ca7ec99a77..739f9f9e0a 100644 --- a/packages/twenty-apps/internal/twenty-partners/package.json +++ b/packages/twenty-apps/internal/twenty-partners/package.json @@ -1,6 +1,6 @@ { "name": "twenty-partners", - "version": "1.1.1", + "version": "1.1.2", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/import-opportunity-from-tft.test.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/import-opportunity-from-tft.test.ts index defa896f31..bb735762d6 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/import-opportunity-from-tft.test.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/import-opportunity-from-tft.test.ts @@ -95,4 +95,28 @@ describe('import-opportunity-from-tft handler', () => { pointOfContactId: 'person-1', }); }); + + it('drops null amountMicros/closeDate instead of failing validation', async () => { + queryMock + .mockResolvedValueOnce({ opportunities: { edges: [] } }) + .mockResolvedValueOnce({ companies: { edges: [] } }) + .mockResolvedValueOnce({ people: { edges: [] } }); + mutationMock + .mockResolvedValueOnce({ createCompany: { id: 'company-1' } }) + .mockResolvedValueOnce({ createPerson: { id: 'person-1' } }) + .mockResolvedValueOnce({ createOpportunity: { id: 'opp-1' } }); + + const body = { ...baseInput(), amountMicros: null, closeDate: null }; + const result = await handler({ + body, + headers: { 'x-application-secret': SECRET }, + }); + + expect(result).toEqual({ ok: true, created: true, id: 'opp-1' }); + const data = mutationMock.mock.calls.find( + ([arg]) => 'createOpportunity' in arg, + )?.[0].createOpportunity.__args.data; + expect(data).not.toHaveProperty('amount'); + expect(data).not.toHaveProperty('closeDate'); + }); }); diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts index 8d864bda95..aa82dd4da4 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts @@ -12,8 +12,20 @@ export const IMPORT_OPPORTUNITY_FROM_TFT_LOGIC_FUNCTION_ID = const APPLICATION_SECRET_HEADER = 'x-application-secret'; +// TFT POSTs `null` for empty fields; treat null as "field absent". +const dropNulls = (value: unknown): unknown => + value === null + ? undefined + : Array.isArray(value) + ? value.map(dropNulls) + : typeof value === 'object' + ? Object.fromEntries( + Object.entries(value).map(([key, val]) => [key, dropNulls(val)]), + ) + : value; + // Request contract — the JSON the TFT workflow POSTs. -export const importOpportunityFromTftSchema = z.object({ +export const importOpportunityFromTftSchema = z.preprocess(dropNulls, z.object({ tftOpportunityId: z.string().optional(), name: z.string().trim().min(1), amountMicros: z.number().optional(), @@ -33,7 +45,7 @@ export const importOpportunityFromTftSchema = z.object({ lastName: z.string().optional(), }) .optional(), -}); +})); export type ImportOpportunityFromTftInput = z.infer< typeof importOpportunityFromTftSchema