fea06bdd4b
**Merge after #23295.** Targets `main`, but must land second: #23295 bumps `1.3.2 → 1.4.0`, and this bumps `1.4.0 → 1.5.1`. Merging this first would leave `main` at 1.5.1 and make #23295's bump conflict and regress the version. `package.json` is the only file the two branches share. App version: **v1.5.1**. ## What this does Posts a Discord notification when a client brief is submitted through the public marketplace form, and records which partner's profile page the brief came from. A visitor can reach the brief form from the marketplace listing page or from a specific partner's profile. Until now that context was lost. This adds a `referredByPartner` relation on Opportunity so the attribution is a queryable CRM fact rather than a line in a chat message. ## How it works `submitClientBrief` resolves the incoming `partnerSlug` to a Partner, sets the relation on create, then posts the embed inline. Inline rather than an `opportunity.created` database trigger, because that event cannot distinguish a brief from a TFT import — both are created by logic functions and both carry `createdBy.source === 'APPLICATION'`. A trigger would need a discriminator like "source is APPLICATION and `tftOpportunityId` is empty", which silently breaks the day a third logic function creates an Opportunity. The cost of going inline is that the Discord call sits in the visitor's request, so it uses a 3s timeout rather than the trigger path's 8s, and every failure is swallowed — a dead webhook can never turn a submitted brief into a failed one. ## Notable decisions - **Slug resolution ignores `validationStage` and `availability`**, unlike the marketplace profile query. If someone submitted a brief from a partner's page, that partner referred it, even if they go unavailable a minute later. Filtering would silently drop real attribution. - **An unresolved slug never fails the brief.** It logs a warning, leaves the relation unset, and still notifies. A brief is a sales lead; losing one over an attribution field the visitor never saw would be a bad trade. - **`referredByPartner` is separate from the existing `partner` field.** One is who sent the lead, the other is who works it. - **The Discord connector moved to `modules/shared/connector/`.** Two domains now need it, and `AGENTS.md` forbids importing logic sideways between domains. `postWebhook` gained `label` and `timeoutMs` parameters; the transport is otherwise unchanged. - Reuses the existing `DISCORD_WEBHOOK_URL` and `PARTNER_APP_FRONTEND_URL` variables — no new configuration to set on prod. ## Permissions `partner.role.ts` locks the new Opportunity field. `configure-partner-rls.ts` treats its skip-list as a closed allowlist of system columns, so an unlocked new field is reported as a discrepancy. Note that Opportunity RLS for partners is `(partnerUser IS me) OR (isListed = true)`, so on a **listed** brief any partner can read `referredByPartner` — i.e. see that a competitor referred it. Called out deliberately; happy to restrict it if that's not wanted. ## Testing 8 unit tests for the embed mapper (partner present/absent, truncation, absent optionals, no email in the payload, inline-row padding) and 4 for the schema. Full suite: 188 passing, lint clean. Verified end to end against a local workspace with a real Discord webhook. All three paths return `ok: true`; the persisted relation was confirmed via GraphQL rather than inferred from the status code: | Submission | `referredByPartner` | |---|---| | valid slug | linked to the partner | | no slug | `null`, embed reads "Marketplace listing" | | unknown slug | `null`, brief still succeeds | ## Follow-up, not in this PR `yarn rls:configure` fails before reaching its field-lock check — its retry path strips `predicateGroups` but the predicates still carry `rowLevelPermissionPredicateGroupId`, so the retry fails identically. Pre-existing and unrelated to this change (`configure-partner-rls.ts` is untouched here), but it means the script cannot currently verify the lock on a fresh workspace. The website side that sends `partnerSlug` is #23351. Until it ships, this is inert: no caller sends the field, and briefs behave exactly as before. Merge this one first — #23351 is the sender, this is the receiver. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23344?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. -->
48 lines
1.4 KiB
JSON
48 lines
1.4 KiB
JSON
{
|
|
"name": "twenty-partners",
|
|
"version": "1.5.1",
|
|
"license": "MIT",
|
|
"engines": {
|
|
"node": "^24.5.0",
|
|
"npm": "please-use-yarn",
|
|
"yarn": ">=4.0.2",
|
|
"twenty": ">=2.23.0"
|
|
},
|
|
"keywords": [],
|
|
"packageManager": "yarn@4.13.0",
|
|
"scripts": {
|
|
"twenty": "twenty",
|
|
"lint": "oxlint -c .oxlintrc.json .",
|
|
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
|
"test": "vitest run",
|
|
"test:unit": "vitest run --project unit",
|
|
"test:integration": "vitest run --project integration",
|
|
"test:watch": "vitest",
|
|
"seed": "tsx src/scripts/seed.ts",
|
|
"seed:prod": "ENV_FILE=.env.prod tsx src/scripts/seed.ts",
|
|
"purge": "tsx src/scripts/purge-soft-deleted.ts",
|
|
"purge:prod": "ENV_FILE=.env.prod tsx src/scripts/purge-soft-deleted.ts",
|
|
"rls:configure": "tsx src/scripts/configure-partner-rls.ts",
|
|
"rls:configure:prod": "ENV_FILE=.env.prod tsx src/scripts/configure-partner-rls.ts"
|
|
},
|
|
"dependencies": {
|
|
"react-markdown": "^10.1.0",
|
|
"twenty-client-sdk": "2.23.0-alpha.2",
|
|
"twenty-sdk": "2.23.0-alpha.2",
|
|
"zod": "^4.1.11"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^24.7.2",
|
|
"@types/react": "^19.0.0",
|
|
"@types/react-dom": "^19",
|
|
"dotenv": "^16.0.0",
|
|
"oxlint": "^0.16.0",
|
|
"react": "^19.0.0",
|
|
"react-dom": "^19.0.0",
|
|
"tsx": "^4.0.0",
|
|
"typescript": "^5.9.3",
|
|
"vite-tsconfig-paths": "^4.2.1",
|
|
"vitest": "^4.0.0"
|
|
}
|
|
}
|