e51efef7c854fe081237802a5d97efdbbed9949b
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
ff5d082e7c |
feat(partners): remove Project Budget Typical field, rework partner views & nav order (#21162)
## Summary Partners-app changes spanning the Partner object, its data scripts, table views, and sidebar navigation. ### Remove the "Project Budget Typical" field Dropped the `projectBudgetTypical` currency field from the Partner object and every reference to it: - `get-partner-by-slug` and `list-available-partners` logic-function selections - the seed script (type, write mapping, and per-partner data) - the `import-from-tft` mapping (also dropping the now-unused `partnerBudgetAverage` TFT source selection) `projectBudgetMin` is intentionally kept. ### Rework partner views - **Partners** (all-partners) view: replaced the **Deployment Expertise** column with **Categories** (the `partnerScope` field). - **Validated partners** view: added a **Languages Spoken** column. - Set view `position`s so the in-object view switcher orders **Validated → Applications → Partners**. ### Navigation order Reordered the "Partners" folder navigation items so the sidebar reads **Validated partners → Partner applications → Partners** (Partner content stays last). ### Also included The previously-pushed fix that excludes partners with an empty slug from the available-partners list. ## Notes - No deploy/sync performed. The view-column and navigation-ordering changes take effect once the app manifest is synced (`yarn twenty dev --once` locally). - The `deploymentExpertise` field itself is unchanged — only its column was removed from the all-partners view. |
||
|
|
ea84aabe4c |
chore(twenty-partners): refine design-doc skill doctrine (#21151)
## Summary Iterative refinements to the partner design-doc doctrine after running it on a second lead (TADA) and reviewing output side by side. Touches only the `twenty-partner-design-doc` skill files (doctrine + Claude Code wrapper); no runtime / app code. **What changed** - **Flag system:** emoji + short text label pairs only (`🔮 inf.`, **❓ open**, **⚠️ heavy**, **🛑 blocker**). Replaces the prior text-tag-only system; scannable, unambiguous. - **Section structure:** split into **Required** (always present) and **Conditional** (Views, Automations, Integrations, Reporting). Include conditional sections only when the client grounded them in the source. Number sequentially, no gaps. - **No filler placeholders:** banned `X was not named` / `left out on purpose` lists in body sections. Unknowns belong in Open questions, not as their own section or bullet. - **Functional cross-refs:** every `§N` reference is now a markdown anchor link `[§N](#n-section-slug)`, so a partner skimming the doc can navigate. Bare `§N` is banned. - **Bullets and tables over paragraphs**, with **Open questions** kept as a numbered list (so the partner can read items 1, 2, 3 with the client). - **Views & navigation** rendered as a tight `Surface | Shows | Audience` table. No view-type column — table / kanban / page layout is the partner's call, not a scoping decision. - **Data-model table** gains a `Source` column (`client` / `inf.`) for at-a-glance fact-vs-inference visibility. - **Business decisions over technical mechanics:** cut SDK / runtime internals that don't move the quote (Docker version, OAuth flavour, auto-system relations, env-var names, CI/CD workflow detail). - **Common-mistakes table** updated with rows for the new rules. - **SKILL.md self-check** expanded so the wrapper enforces all of the above before saving. ## Test plan - [ ] Re-read doctrine end-to-end for internal consistency - [ ] Verify the four canonical emoji + text pairs appear and no stray emoji flags remain - [ ] Confirm Required vs Conditional structure is internally consistent (no section listed in both) - [ ] Confirm functional-cross-ref rule appears in both Rules and Formatting and is reflected in the SKILL.md self-check - [ ] Confirm Views & navigation entry mandates the three-column table and bans a Type column - [ ] Confirm Common-mistakes table covers each new rule |
||
|
|
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 |
||
|
|
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. |
||
|
|
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) |
||
|
|
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 |