24533b510c
## Summary Restructures the Twenty Partners app into an **Application-driven matching workspace**: leads post briefs (Opportunities), partners browse and **self-apply**, admins review applications and assign a winner. The candidacy funnel lives on `Application.state`; the deal lifecycle lives on the stock Opportunity `stage`. Version **1.0.0** — **breaking**: removes the legacy `matchStatus` field and the auto-match flow. Prod upgrade path is **uninstall → deploy → install** (not an in-place upgrade). ## How "Apply" works — no workflow, no special permission Partners apply by **creating an Application directly** from a listed brief (a normal record write, governed by the Application object permission). The `on-application-created` logic function (shipped in the manifest) then resolves the partner from `createdBy`, sets `state = APPLIED`, stamps `partner`/`partnerUser`/`lastActivityAt`, and dedupes by (opportunity, partner). - **No `WORKFLOWS` permission flag.** An earlier iteration used a manual "Apply" workflow, but running a manual workflow requires the `WORKFLOWS` flag, which **cannot be granted on an app-owned role** (the manifest sync drops `role → permissionFlag` links, and the metadata API rejects out-of-band grants on app roles). Self-apply via record-create sidesteps this entirely and is prod-viable as-is. - `Application.state` **defaults to `APPLIED`** so a partner never sees a misleading "Invited" flicker while the async handler runs. Admin invites set `INVITED` explicitly. ## ⚠️ Manual setup after install (per workspace) 1. **`yarn rls:configure`** — applies the partner row-level predicates and verifies field-locks (predicates can't ship in the manifest). Required for partner scoping. 2. **"Mark as Winner" workflow** — one manual-trigger workflow on **Application** → *Update Record* that sets `Opportunity.partner`, which drives the WON/BACKUP cascade. Admins run it (admins bypass the flag via `canUpdateAllSettings`); equivalent to editing the Opportunity's `partner` field directly. Steps in `src/workflows/README.md` (the **Apply** section there is superseded by self-apply). ## What's included - **Data model:** new `BACKUP` Application state; symmetric cascade owned by `on-opportunity-partner-won` — assign → winner `WON`, other applicants `BACKUP`; unassign → all reopen to `APPLIED`. `Opportunity.partner` is the single source of truth. - **Removed:** `matchStatus` field + `on-opportunity-auto-match` (dead). Deal lifecycle now on the stock `stage`. - **Partner row-level security (B7):** RLS predicates scope partners to their own `Partner`/`Person`/`Company`/`Application` rows; `Opportunity` is `(partnerUser IS me) OR (isListed = true)` so listed briefs are visible to all partners; `Application` is `(partnerUser IS me) OR (lastActivityAt IS EMPTY)` — the IS-EMPTY branch lets a partner's own insert pass (partnerUser is stamped just after insert). Field-locks make Opportunity `stage`/`amount` and most Application fields read-only for partners (pitch stays editable). Applied via `yarn rls:configure`. - *Trade-off:* an unstamped application (lastActivityAt null) is briefly readable by any partner — sub-second window, permanent only if the handler fails to stamp. Acceptable for an internal marketplace; the front-component Apply path (below) would remove it. - **Idempotency:** `on-application-created` dedupes duplicate applications by (opportunity, partner). - **Views & navigation**, reorganized into sections: - **Partner Workspace:** Open Briefs · My Applications · My Profile · My Deals - **Matching Admin:** Briefs to Match · Deals (board) · Applications · Applications by Opportunity · All Opportunities · Follow-up Applications · Follow-up Briefs - **Partners:** per Stage · per Country · Partner Applications · Validated (per-group COUNT) - Opportunity & Partner record **side panels** via FIELDS_WIDGET views (surface relations incl. `applications`, so a brief shows all its applications). ## Known issues / follow-ups - **Pre-existing failing unit test (not introduced here):** `on-partner-application-created › "posts a Discord embed when an APPLICATION-sourced partner is created"` — the handler/test are byte-identical to base; tracked separately. - **Follow-up views** lack the "older than 7 days" staleness filter — no confirmed relative date operand in this Twenty version (TODOs left in the views). - **Apply UX (future):** a front-component "Apply" button on the brief, calling an authenticated `/s/apply-to-brief` logic function (runs as the app), would replace the "create a record" entry point — nicer UX, and it removes the RLS IS-EMPTY trade-off. Not required to ship. ## Testing - Partner self-apply verified end-to-end as a partner (create application from a brief → lands on `APPLIED`). - Unit tests for the WON/BACKUP cascade + application handlers pass (the one failing test above is the pre-existing, unrelated Discord handler). - Lint clean (`oxlint`). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21816?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-light.svg"></picture></a> <!-- End of auto-generated description by cubic. -->