From 6ae6703e7d3b2c97fbbf1424cd6fa3a1c0b0b1ab Mon Sep 17 00:00:00 2001 From: Rashad Karanouh <11599358+rashad@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:43:46 +0400 Subject: [PATCH] Map TFT useCase to partners need on opportunity import (#22054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Accept optional `useCase` in the TFT → partners `import-opportunity-from-tft` webhook payload - Map it to `Opportunity.need` on create (TFT Use Case → partners Needs) - Add unit tests for happy path and null `useCase` handling - Bump twenty-partners to 1.1.4 (patch) ## Test plan - [x] `yarn test:unit` (43 tests passing) - [x] `yarn lint` (0 errors) - [ ] Deploy to local/partners workspace with `yarn twenty dev --once` - [ ] POST smoke test with `useCase` in body; confirm **Need** field populated - [ ] TFT workflow: add `"useCase": "{{record.useCase}}"` to HTTP body (manual) Review in cubic --- .../internal/twenty-partners/package.json | 2 +- .../import-opportunity-from-tft.test.ts | 29 ++++++++++++++++++- ...ort-opportunity-from-tft.logic-function.ts | 4 +++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/twenty-apps/internal/twenty-partners/package.json b/packages/twenty-apps/internal/twenty-partners/package.json index 341d5b3b22..8c03674f2c 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.3", + "version": "1.1.14", "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 bb735762d6..debd417db6 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 @@ -78,7 +78,9 @@ describe('import-opportunity-from-tft handler', () => { .mockResolvedValueOnce({ createPerson: { id: 'person-1' } }) .mockResolvedValueOnce({ createOpportunity: { id: 'opp-1' } }); - const result = await handler(authedEvent(baseInput())); + const result = await handler( + authedEvent(baseInput({ useCase: 'Migrate from HubSpot, ~30 sales users' })), + ); expect(result).toEqual({ ok: true, created: true, id: 'opp-1' }); @@ -91,6 +93,7 @@ describe('import-opportunity-from-tft handler', () => { amount: { amountMicros: 5000000, currencyCode: 'EUR' }, closeDate: '2026-07-01T00:00:00.000Z', stage: 'MEETING', + need: 'Migrate from HubSpot, ~30 sales users', companyId: 'company-1', pointOfContactId: 'person-1', }); @@ -119,4 +122,28 @@ describe('import-opportunity-from-tft handler', () => { expect(data).not.toHaveProperty('amount'); expect(data).not.toHaveProperty('closeDate'); }); + + it('drops null useCase 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(), useCase: 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('need'); + expect(data).not.toHaveProperty('useCase'); + }); }); 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 aa82dd4da4..0b43b2c11d 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 @@ -32,6 +32,7 @@ export const importOpportunityFromTftSchema = z.preprocess(dropNulls, z.object({ currencyCode: z.string().optional(), closeDate: z.string().optional(), stage: z.string().optional(), + useCase: z.string().optional(), company: z .object({ name: z.string().optional(), @@ -198,6 +199,9 @@ export const handler = async ( if (isNonEmptyString(input.stage)) { opportunityData.stage = input.stage as CoreSchema.OpportunityStageEnum; } + if (isNonEmptyString(input.useCase)) { + opportunityData.need = input.useCase.trim(); + } if (companyId !== undefined) opportunityData.companyId = companyId; if (pointOfContactId !== undefined) opportunityData.pointOfContactId = pointOfContactId;