Commit Graph

7 Commits

Author SHA1 Message Date
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
Rashad Karanouh 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.
2026-06-02 20:19:07 +00:00
Rashad Karanouh 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
2026-06-02 12:47:56 +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