Commit Graph

17 Commits

Author SHA1 Message Date
Rashad Karanouh 0e97e1a908 feat(partners): add partner-application-triage and partner-meeting-recap skills (#21819)
## What

Adds two Twenty partner-pipeline skills to
`twenty-partners/src/skills/`, plus a patch version bump.

### `twenty-partner-application-triage`
Ranks the partner-application backlog by net-new value and surfaces a
short chase-list of high-value applicants who haven't booked a call.
Read-only against the live partners workspace. Ships `rank.py` as its
scoring helper.

### `twenty-partner-meeting-recap`
After partner calls, pulls Fireflies meetings, matches each to an
existing Partner by attendee email/domain, writes a recap
(transcript-first, Fireflies summary as fallback), and injects it as a
Note linked to the partner via `NoteTarget`. Skips leads/discovery calls
(no Partner match) and meetings whose content isn't ready yet. Optional
`--prune` deletes the Fireflies recording once its recap is safely in
the CRM (confirmed first).

## Version
`twenty-partners` 0.5.4 → **0.5.5** (patch: additive skill docs, no app
behaviour change).

## Notes
- Both skills read credentials from `~/.twenty/credentials.env`; no
secrets committed.
- All GraphQL queries/mutations are the proven ones used against the
live workspace.
2026-06-19 09:35:47 +02:00
Rashad Karanouh a7760c04ab Partners app: profile picture (additive file field), derived region & deployment, scope cleanup (0.5.4) (#21709)
## Summary (twenty-partners app, v0.5.4)

- **Profile picture upload (additive)**: `profilePicture` stays a URL
(LINKS) — existing partners keep their picture — and a new
`profilePictureFile` (FILES) field is added for uploads. The read logic
functions (`list-available-partners`, `get-partner-by-slug`) select both
and **prefer the uploaded file, falling back to the legacy URL**,
returning the existing `{ primaryLinkUrl }` shape so the public
directory and the website are unchanged.
- **Region** auto-derived from the partner's country on application
creation (static lookup).
- **Deployment expertise** derived: defaults to `CLOUD`, adds
`SELF_HOST` when the partner covers Hosting & Infrastructure.
- **Partner.website** now set from the submitted domain.
- Removed 5 unused `partnerScope` categories (0 production usage); seed
remapped.
- Removed one-off data scripts (`import-from-tft`,
`migrate-partner-scope`, `partner-scope-map`).

Rebased on `main` (includes #21615 company-reuse).

## Why additive, not a field-type change

Twenty treats a field's `type` as **immutable**: an app upgrade silently
ignores a LINKS→FILES change (`fieldMetadata.type` is `toCompare: false`
in the server's flat-entity config). An in-place flip would leave the
column LINKS on prod while the display queries asked for a FILES `url`,
**breaking the partner directory**. The additive `profilePictureFile`
upgrades cleanly with no data loss; existing URLs keep working via the
legacy field + fallback. Removing the 5 unused enum options is also a
clean upgrade (0 records use them).

## Deploy notes

- Version `0.5.4`. Fully additive schema change → installs in place, no
data migration required.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21709?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. -->
2026-06-17 10:04:23 +00:00
Rashad Karanouh 065b6efe11 fix(twenty-partners): reuse existing company by domain in partner-application handler (#21615)
## Problem

Partner applications **502** for any applicant whose company is already
in the CRM.

The `submit-partner-application` logic function dedupes applicants
**only by person email**. When no person matches that email, it takes
the create path and calls `createCompany` unconditionally. But
`Company.domainName` has a **UNIQUE index**, so whenever a company with
the applicant's domain already exists — which is common, since the **TFT
import seeds companies** — the mutation throws `"duplicate entry"`. The
handler's `catch` returns `{ ok: false }`, and the website
`/api/partner-application` route surfaces it as a **502**. The applicant
can never be submitted.

Real case that surfaced this: an applicant whose company (`BKG
Integration UG`, domain `bkg-integration.de`) was already present from
the TFT import with no Partner/Person attached.

## Fix

Extract `findOrCreateCompanyId`:
- Look the company up by **exact domain** (`domainName.primaryLinkUrl
eq`) and **reuse** it when found.
- Only `createCompany` when no domain matches.
- The matched company is **never renamed** — the existing CRM name wins
over the applicant's free-text `companyName`.

Person-email dedup is unchanged (already handled upstream in the
handler).

### Known limitation
Matches **active** rows only. A *soft-deleted* company still holds the
unique index and would re-collide; clear those with `yarn purge:prod`.
Noted inline.

## Tests
Adds an integration test: pre-seed a company by domain → submit an
application with the same domain → assert the partner reuses the same
company id and the company name is untouched.

## Version
`twenty-partners` 0.5.1 → **0.5.2** (patch: bug fix, no schema change).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21615?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. -->
2026-06-16 07:17:29 +00:00
Charles Bochet a3fe9efb69 chore(apps): bump twenty-sdk & twenty-client-sdk to 2.13.0, vitest to 4 (#21553)
## What

Bumps **all 14 `twenty-apps`** (internal, examples, community) to the
freshly published SDK **2.13.0**, and upgrades `vitest` 3 → 4 so `vite`
resolves to 8 (rolldown).

- `twenty-sdk` / `twenty-client-sdk`: `2.10.1` → `2.13.0` (each app's
original spec format preserved — plain, `^`, and `npm:…@`).
- `vitest`: `^3.x` → `^4.0.0` in the 13 apps that use it
(`call-recording` has no vitest).

## Why

Each app's `yarn.lock` had open Dependabot esbuild alerts — high
`GHSA-gv7w-rqvm-qjhr` and low `GHSA-g7r4-m6w7-qqqr`, both fixed in
esbuild `0.28.1`.

The SDK bump alone does **not** clear them: the advisories fire on *any*
esbuild `< 0.28.1`, and each app pulled a vulnerable `esbuild@0.27.7`
transitively via **vite** (through the `vitest` devDependency),
independent of the SDK. Bumping `vitest` to 4 resolves `vite@8`
(rolldown), which drops the esbuild dependency entirely.
`twenty-partners` additionally needed a recursive esbuild re-resolution
(its `tsx` dep had `esbuild@~0.28.0` pinned at the still-vulnerable
`0.28.0`).

After this change, **all 14 lockfiles resolve esbuild `0.28.1` only** —
zero copies `< 0.28.1`.

## Test

- All 14 lockfiles verified free of esbuild `< 0.28.1`.
- vitest 4 + vite 8 confirmed working: `people-data-labs` runs **334
tests across 83 files, all passing**.
- `twenty-for-twenty`'s suite fails only because its global setup
requires a live Twenty server (`/healthz`) — environmental, would fail
identically under vitest 3.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21553?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. -->
2026-06-14 20:50:57 +02:00
Rashad Karanouh adbd78767e feat(partners): lock admin-managed + ownership fields on Partner role (#21471)
## What

Tightens the **Partner** self-service role's field-level permissions so
a partner can edit its own profile but not admin/ops-controlled or
ownership fields. All locks are `canUpdateFieldValue: false` on the
Partner object.

**Admin-managed scalar fields (7):** `slug`, `validationStage`,
`reviewed`, `ranking`, `partnerTier`, `applicationNotes`, `lastMatchAt`

**Ownership relation FKs (2):** `partnerUser`, `company`

## Why

- The 7 scalar fields are admin/ops-controlled (validation, ranking,
tiering, internal notes) — a partner must not be able to self-promote or
alter ops data.
- `partnerUser` is the **RLS pivot**: the row-level predicate scopes a
partner to records where `partnerUser IS <their workspace member>`. If a
partner could clear or repoint it, they'd drop their own record out of
scope (an orphan only admins can see). It is already locked on
Opportunity; this brings Partner in line.
- `company` is read-only at the object level for partners, so its FK
link must not be repointable from the Partner side either.

The remaining Partner relations (`opportunities`, `persons`,
`partnerContents`) need no lock — they are already protected by
inverse-side field locks or object-level read-only / no-access rules.

## Scope

- One source file: `src/roles/partner.role.ts` (9 field-permission
entries).
- No schema changes — additive permission tightening; upgrades cleanly
via `deploy` + `install`.
- Version: patch bump `0.5.0 → 0.5.1`.
2026-06-12 07:24:15 +00:00
Rashad Karanouh f7463886a6 feat(partners): partner role row-level security (RLS) with scoped edits (#21386)
## Summary
Adds an external **Partner** self-service role that sees and edits only
its own
records via row-level security (RLS), so a validated partner can sign in
and manage
just the deals they're matched on.

## What's included
- **`partnerUser` relation** on Partner, Person, Company, Opportunity (+
inverse
  relations on Workspace Member) — the login member a record belongs to.
- **RLS predicates** scoping each of those objects to "partnerUser IS
the current
workspace member", plus a self-scope on Workspace Member so member-typed
relations
resolve without exposing the internal team roster. Applied out-of-band
via
  `yarn rls:configure` (the app manifest cannot ship RLS predicates).
- **Assign / unassign cascade** (`on-opportunity-partner-assigned` logic
function):
assigning a Partner to an Opportunity stamps `partnerUser` onto the
Opportunity +
its Company + People; removing the Partner clears it (and cascades to
the
  Company/People when no other deal of that member still uses them).
- **Partner role permissions**
  - Partner profile: full read/update.
- Opportunity: read all; **update `stage` and `amount` only** (every
other
    user-facing field locked).
  - Company / Person: read-only.
  - Workspace Member: read-only, RLS-scoped to self.
- **`partnerUser` column** added to the Validated Partners view so the
login member
  can be assigned inline.

## Install / upgrade note
After install or reinstall, run `yarn rls:configure` (`:prod` variant
for prod) to
(re)apply the RLS predicates and verify the field-permission locks.
Manifest sync
handles object/field permissions; predicates are applied by this script.

## Platform gaps found (for the eng team)
1. **Manifest sync doesn't invalidate the roles-permissions Redis
cache.** Permission
changes deployed via `yarn twenty dev --once` persist to the DB but
aren't reflected
   in the cached snapshot used for enforcement until

`engine:workspace:metadata:permissions:roles-permissions:<workspaceId>:{data,hash}`
   is flushed. Relevant on any real workspace when permissions change.
2. **Locking a server-injected field silently breaks all updates.** The
`*.updateOne`
pre-query hook writes `updatedBy` into every update, so
`canUpdateFieldValue:false`
on `updatedBy` makes the permission check reject *every* record update
with
`PERMISSION_DENIED`. Field-permission lock lists must exclude
server-managed/injected
fields (`updatedBy`; and `position`, co-written with `stage` on kanban
drag).

## Version
Minor bump → `0.5.0` (new role, new fields, new behaviour;
backwards-compatible).

## Testing
- Verified locally as a partner user: edits own profile; edits
Opportunity stage +
amount; Company/Person read-only; sees only matched deals; unassigning a
partner
  removes the deal (and its company/people) from the partner's view.
- `yarn rls:configure` passes (5 predicates upserted; 24 Opportunity
fields locked,
  stage + amount editable).
- Lint clean.
2026-06-11 11:37:35 +00:00
Rashad Karanouh 99bd1daaec feat(twenty-partners): website field + restructured partner & opportunity views (#21385)
## What

Twenty Partners app — partner enrichment + a simpler, partner-centric
view structure.

**Partner**
- New `website` (LINKS) field.
- Icons for the opportunity `partner` relation and `matchStatus` fields
(were the default "123").

**Partner views** (Partners folder → Applications / Validated / All)
- **Applications**: grouped by validation stage, showing Application +
Potential; columns Categories / Skills / Type of Team / Languages /
Country / LinkedIn.
- **Validated**: grouped by availability; columns harmonized with
Applications, plus Partner Tier.
- Wider Name / Categories columns.

**Opportunity pipeline** (Pipeline folder, simplified by partner
presence)
- `OPP without partner` / `OPP with partner` tables (filtered on partner
*is empty* / *is not empty*), `OPP all`, and a kanban **board** grouped
by match status.
- Sort by match status (uses the option position, i.e. pipeline order).
- Removes the previous matching views (waiting-for-match,
matches-overview, partner-deals).

## Version
Patch bump `0.4.2 → 0.4.3` (relative to main).
2026-06-10 09:26:01 +00:00
Charles Bochet a48c158a66 security(apps): bump twenty-sdk to 2.10.1 across twenty-apps (tmp, undici) (#21344)
## Summary

Propagates the just-published **`twenty-sdk@2.10.1`** security patch
into the `twenty-apps/*` mini-apps, clearing the bulk of the
nested-lockfile Dependabot alerts (the `tmp` + `undici` clusters).

Each app carries its own `yarn.lock`, so the fix only reaches them once
they bump the SDK. `2.10.1` drops the two vulnerable transitive deps
every app inherited:

| Vuln dep | Source | Fixed by |
|---|---|---|
| `tmp@0.0.33` (GHSA-ph9p / GHSA-52f5) | `inquirer ^10 →
external-editor` | `inquirer ^14` → `@inquirer/editor@5` (no
external-editor) |
| `undici@<6.24` (5 GHSAs) | `@genql/cli` | vendored genql codegen
(`@genql/cli` removed) |

## Changes
Bumps `twenty-sdk` **and** `twenty-client-sdk` (whichever each app pins
— several pin both) to `2.10.1` and regenerates each lockfile.

**10 apps updated** (all on the v2 line — minor bump, low risk):
`twenty-slack`, `twenty-discord`, `twenty-linear`, `twenty-partners`,
`twenty-fireflies`, `people-data-labs`, `twenty-for-twenty`, `exa`,
`github-connector`, `postcard`.

Verified per-app after regen: **`tmp@0.0.33` = 0** and **`undici@5` =
0** in every updated lockfile.

## Deliberately excluded
Three apps pin a **pre-2.0** SDK, where `→ 2.10.1` is a major jump that
risks breaking the app and needs per-app validation:
- `examples/hello-world` (`0.9.0`)
- `internal/call-recording` (`0.6.3-alpha`)
- `internal/self-hosting` (`1.22.0-canary.6`)

These still carry one `tmp`/`undici` alert each and should be handled in
a follow-up.

## Related
- `twenty-sdk@2.10.1` release (tag `sdk/v2.10.1`) — backport of #21339
(undici) + #21340 (tmp) from `main`.
2026-06-08 21:31:14 +02:00
Rashad Karanouh bf75ab8982 feat(twenty-partners): notify Discord on new partner application (#21313)
Adds an `on-partner-application-created` logic function triggered on the
`partner.created` database event. When the website application form
creates a new Partner, it posts a rich embed to a Discord channel
(applicant, company, country, languages, partner scope, skills) with a
deep link to the record.

## How it works
- Fires only on genuine form submissions — discriminates via
`createdBy.source === 'APPLICATION'`, which excludes seed/import (`API`)
and manual UI (`MANUAL`) creation.
- Runs out-of-band on the worker (database event trigger), so it adds
**no latency** to the applicant's submission, and the linked Person
already exists by the time it runs.
- Best-effort: a Discord failure never fails the trigger (wrapped in
`try/catch`, 8s timeout).

## Configuration (per workspace — Settings → Apps → Twenty Partners →
Variables)
- `DISCORD_WEBHOOK_URL` (secret) — the incoming webhook URL. **The
feature is a no-op when unset.**
- `PARTNER_APP_FRONTEND_URL` — workspace front-end base URL for the
record deep link (e.g. `https://partners.twenty.com`).

## Notes
- New logic function + two application variables; version bumped to
**0.4.0** (minor).
- Unit tests cover the source-guard branches, the on/off switch, the
embed contents/ordering, and best-effort failure handling.
- The website and the existing `submit-partner-application` handler are
untouched.
2026-06-08 18:20:18 +00:00
Charles Bochet 5a2523f533 chore(apps): bump vitest to 3.2.6 in twenty-apps projects (GHSA-5xrq-8626-4rwp) (#21336)
Resolves the **vitest Critical** Dependabot alerts
(`GHSA-5xrq-8626-4rwp`, vitest `< 3.2.6`) — #1422–#1433.

Each `packages/twenty-apps/*` project is an **independent yarn project**
with its own `package.json` + `yarn.lock` (not part of the root
workspace). 12 of them declared `vitest: ^3.1.1` and locked an older
3.2.x. This bumps the range to `^3.2.6` and refreshes each lockfile to
**3.2.6** (latest 3.x, published 2026-06-01).

Projects updated: `community/github-connector`,
`examples/{hello-world,postcard}`,
`internal/{exa,people-data-labs,self-hosting,twenty-discord,twenty-fireflies,twenty-for-twenty,twenty-linear,twenty-partners,twenty-slack}`.

- Dev-scope only (test runner); no runtime impact.
- The **root workspace already uses vitest 4.x** (≥ the fix) and is
intentionally untouched.
- Verified: no `vitest < 3.2.6` remains in any `twenty-apps` lockfile.
2026-06-08 19:36:42 +02:00
Charles Bochet 0403762516 security: refresh twenty-apps lockfiles for vulnerable transitive deps (#21316)
## What

The standalone apps under `packages/twenty-apps/*` each ship **their own
`yarn.lock`** (they're not part of the root workspace), and those
lockfiles still pulled vulnerable transitive versions of `axios`,
`undici`, `tmp`, `qs`, `ws`, `brace-expansion`, `uuid` (via `twenty-sdk`
/ `twenty-client-sdk`). This was ~130 of the open Dependabot alerts —
none of them reachable from the root-lockfile PRs.

Ran `yarn up -R` per app to re-resolve the vulnerable transitives within
their existing ranges, across all 13 flagged apps:

- **`axios` → 1.17.0** — clears the entire proxy-auth-leak / ReDoS /
config-merge MITM advisory set (the 56 axios alerts)
- **`qs`, `brace-expansion`, `uuid`** → patched
- **`undici`, `ws`** → patched on the in-range majors (older majors that
parents pin exactly remain, same situation as the root lockfile)

## Scope
- **Lockfile-only**, 13 apps. No `package.json` changes.
- Test **fixtures** (`packages/twenty-apps/fixtures/*`) intentionally
left untouched — Dependabot didn't flag them and they back snapshot
tests.
2026-06-08 16:45:19 +02:00
Rashad Karanouh c2ad3f3614 (partners): bump app version 0.3.3 -> 0.3.4 (#21167)
Forgot to bump the version when merging #21162 from `0.3.3` to `0.3.4`
2026-06-03 07:34:28 +00:00
Rashad Karanouh e721ebe300 chore(twenty-partners): bump app version to 0.3.3 (#21140)
Bumps the `twenty-partners` SDK app version 0.3.2 → 0.3.3 so `main`
tracks what's deployed to prod.

This is the deploy version for the partner-app changes that just landed:
marketplace `partnerScope` exposure (#21126), the
`submit-partner-application` endpoint + new Partner categories +
migration (#21040), the marketplace card rebind (#21127), and the signup
wizard (#21039).

No code changes — version bump only.
2026-06-02 11:42:06 +00:00
Rashad Karanouh 4f47885054 feat(twenty-partners): submit-partner-application HTTP logic function (#21040)
## Summary

Adds a public `POST /partner-applications` HTTP logic function on the
twenty-partners SDK app that receives applications from the website
wizard and idempotently upserts the Partner / Person / Company graph in
the partners workspace. Also introduces the validated **Category**
taxonomy on `partnerScope` (additive, prod-safe) plus the legacy→new
migration tooling.

Companion PR (website side): #21039

### Logic function
- `defineLogicFunction({ httpRouteTriggerSettings: { path:
'/partner-applications', httpMethod: 'POST', isAuthRequired: false,
forwardedRequestHeaders: ['x-application-secret'] } })`.
- Authenticates via shared-secret header (`X-Application-Secret` ↔
`PARTNER_APPLICATION_SECRET` workspace variable). Twenty's
`isAuthRequired: true` only accepts user-session JWTs, so the handler
enforces auth itself.
- Idempotent upsert keyed on `Person.emails.primaryEmail`:
  - missing email → create Company → Person → Partner
  - existing Person, no Partner → create Company + Partner, link
- existing Person + Partner → update Partner fields; preserve
staff-owned columns (`validationStage`, `reviewed`, `ranking`,
`partnerTier`, `lastMatchAt`) by omitting them from the update
- Create-time defaults preserved on resubmit: `slug =
slugify(companyName)` ("YC Agency" → "yc-agency"), `reviewed = false`,
`partnerTier = 'NEW'`.
- Currency conversion to `{ amountMicros, currencyCode: 'USD' }` for
`hourlyRate` + `projectBudgetMin`.

### Categories (`partnerScope`) — additive, prod-safe
- Adds 5 validated category options — `ADVISORY`, `SOLUTIONING`,
`DEVELOPMENT`, `HOSTING`, `SUPPORT` — to the `partnerScope` MULTI_SELECT
**without removing** the legacy options (there is production data on
them). Field relabeled **"Categories"**. The website form only emits the
new values.
- **Migration tooling** (run deliberately, *not* in CI):
`scripts/migrate-partner-scope.ts` remaps existing records legacy→new —
dry-run by default, `MIGRATE_APPLY=1` to write, two-pass
(collect-then-apply, no mutate-while-paginating).
`scripts/partner-scope-map.ts` is the single mapping source;
`import-from-tft.ts` now routes imported scope through it so the TFT
import never re-introduces retired values. Removing the legacy options
is deferred until after the migration has run + been verified.

### applicationNotes
- New `applicationNotes` TEXT field holds the wizard's single free-text
"anything else" note (the handler passes it through directly).
`deploymentExpertise` was dropped from the handler
input/validation/builders (the column is retained for now, pending the
same migration cleanup).

### Application variable
- Declares `PARTNER_APPLICATION_SECRET` with `isSecret: true` so each
workspace sets the value via Settings → Apps → Twenty Partners →
Variables. Twenty encrypts at rest and merges the decrypted value into
the handler's `process.env` at execution time (workspace value wins over
container env).

### Code quality (from review)
- One shared `slugify` (`scripts/slugify.ts`, the import's algorithm)
used by both the handler and the import, so the `slug` identity key
can't diverge across paths.
- Unit-test tier: `vitest.unit.config.ts` (no `globalSetup`) + `yarn
test:unit`, so the pure `mapLegacyScope` test runs without a live server
(the integration suite stays server-backed).

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

### Tests
- Integration tests against a local Twenty workspace:
missing-/wrong-secret auth rejections, create flow (asserts slug +
`reviewed: false` + `partnerTier: 'NEW'`), update-on-resubmit +
staff-column preservation, new category values stored,
`applicationNotes` stored, bad-input shape.
- Pure `mapLegacyScope` unit test via `yarn test:unit` (no server).

## Test plan

- [ ] Install / upgrade the app on the target workspace; set
`PARTNER_APPLICATION_SECRET` in Settings → Apps → Twenty Partners →
Variables
- [ ] `curl -i -X POST <workspace-url>/s/partner-applications -H
'X-Application-Secret: <secret>' -H 'Content-Type: application/json' -d
'{"firstName":"Test","lastName":"User","email":"test@example.com","companyName":"YC
Agency","partnerScope":["ADVISORY"],"applicationNotes":"hi"}'` →
`HTTP/1.1 201` + `{"ok":true,"created":true,"partnerId":"..."}`
- [ ] Partner record shows `name: "YC Agency"`, `slug: "yc-agency"`,
`validationStage: APPLICATION`, `reviewed: false`, `partnerTier: 'NEW'`,
`partnerScope: ["ADVISORY"]`, `applicationNotes: "hi"`
- [ ] Re-curl same email with `city: "Paris"` → `created: false`,
`Partner.city` updated, staff-owned columns untouched
- [ ] Wrong / missing secret → `200` +
`{"ok":false,"reason":"unauthorized"}`
- [ ] `yarn test:unit` green (no server); `yarn migrate:partner-scope`
dry-run lists any legacy→new remaps without writing
2026-06-02 10:35:28 +00:00
Rashad Karanouh a3557373e6 feat(twenty-partners): expose partnerScope on list + by-slug endpoints (#21126)
## What

Adds `partnerScope` (the partner **Categories** multi-select) to the
output of the two public partner endpoints:
- `list-available-partners` (`/s/partners`)
- `get-partner-by-slug` (`/s/partner-by-slug`)

Additive only — `deploymentExpertise` is kept, so existing consumers
(the current live marketplace) are unaffected.

## Why

Part of the partner marketplace rework. The website marketplace
(companion branch `rk-rework-marketplace-cards`) consumes `partnerScope`
to show/filter partner Categories. The new options + migration live in
the signup app PR #21040.

## Merge order (we'll decide)

Independent diff — can merge in any order. Couplings to keep in mind:
- **Version line:** this branch and #21040 both bump the app
`package.json` version; whoever merges second re-bumps.
- **Deploy (not merge):** the partners app is deployed manually. Deploy
the final combined app (this + #21040) and run `yarn
migrate:partner-scope:prod` **before** the website is deployed.
2026-06-02 10:23:51 +00:00
Rashad Karanouh 53392f9a16 feat(twenty-partners): partnerContent catalog + TFT import improvements (#20904)
## Summary

Two related threads for the internal `twenty-partners` app:

1. **Redesign `partnerQuote` → `partnerContent`.** The object was
mis-modeled as a sales/pre-invoice doc (`amount`, opportunity link). In
TFT it's actually a marketing-content catalog — customer quotes, case
studies, partner quotes, logos — moving through a production lifecycle.
This renames it in place and reshapes it to mirror TFT's
`CustomerContent`.
2. **Import tooling improvements** to the TFT importer + multi-env
workflow.

## Changes

**Schema (`partnerContent`)**
- Rename `partnerQuote` → `partnerContent` (object, view, nav, relation
fields, identifiers).
- Add `contentType` MULTI_SELECT `[CUSTOMER_QUOTE, CASE_STUDY,
PARTNER_QUOTE, LOGO]` and `interview` LINKS.
- Add `customerCompany` / `customerPerson` relations; keep `partner`;
drop the `opportunity` link (TFT has none).
- Drop `amount`; rename the FILES field `quoteFile` → `documents`
(`attachments` is a reserved morph-relation name).

**Importer (`import-from-tft.ts`)**
- Import the full content catalog (all types), not just `PARTNER_QUOTE`.
- Map TFT `partnerTimezone` → `region`, default
`languagesSpoken=[ENGLISH]`, and set `deploymentExpertise=[SELF_HOST]`
when scope includes `HOSTING_ENVIRONMENT`.
- Filter to partner-relevant records only: opportunities linked to a
partner (20 of 164), content linked to a partner (10 of 22). Drops
general sales-pipeline / customer-only noise.
- Dedupe companies by **normalized domain** (Twenty's unique key), not
just name — fixes duplicate-entry crashes when the same company arrives
under different names.
- Progress logging throughout.

**Tooling**
- `purge-soft-deleted` script (soft-deleted rows block re-imports via
unique constraints).
- Multi-env script variants (`*:prod`) selected via `ENV_FILE`.

## Testing

Verified on a local Twenty instance and on `partner.twenty.com`:
- 122 partners, 20 partner-linked opportunities, 10 partner-linked
content (all types), 229 domain-deduped companies.
- Schema confirmed via metadata introspection; `yarn twenty typecheck`
clean.

## Notes

- Renaming an installed object isn't a pure in-place migration on a
server that already had `partnerQuote` — the working path is `uninstall
→ deploy → install` (safe here: prod had no data).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-26 14:09:57 +00:00
Rashad Karanouh 11b9f708d6 feat(twenty-partners): add partners app (#20792)
## Summary

- Adds `twenty-partners`, a Twenty app that manages the partner matching
pipeline: intake partner-eligible deals, assign vetted marketplace
partners, and track the full funnel
- Custom `Partner` object with availability, geo/language coverage,
deployment expertise, and Calendly link
- `matchStatus` SELECT field on Opportunity — 10 non-nullable states
from `TO_BE_MATCHED` through `WON`/`LOST`, replacing a legacy boolean
approach
- Auto-match logic function: when `matchStatus` → `AUTO_MATCH`, assigns
the longest-idle available partner and advances to `MATCHED`; falls back
to `MANUAL_MATCH` with an audit note if no partner is free
- Views: Waiting for match, Matches overview (Kanban by `matchStatus`),
All matched deals, Partners, Opportunities
- Roles: Partner Ops (internal, full CRUD) and Partner (external
placeholder)
- Idempotent seed scripts for demo partners and pipeline data

## Test plan

- [ ] App installs cleanly on a fresh workspace (`yarn twenty dev`)
- [ ] `matchStatus` Kanban grouping renders correctly in Matches
overview
- [ ] Waiting for match view filters to `TO_BE_MATCHED` and
`MANUAL_MATCH` only
- [ ] Auto-match logic assigns a partner and advances status
- [ ] Seed scripts run without errors and are safe to re-run
2026-05-21 19:14:20 +00:00