Map TFT useCase to partners need on opportunity import (#22054)

## 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)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22054?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Rashad Karanouh
2026-06-24 13:43:46 +04:00
committed by GitHub
parent 269c8ef400
commit 6ae6703e7d
3 changed files with 33 additions and 2 deletions
@@ -1,6 +1,6 @@
{
"name": "twenty-partners",
"version": "1.1.3",
"version": "1.1.14",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -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');
});
});
@@ -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;