Skip to content

Commit a355737

Browse files
authored
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.
1 parent d0e0e27 commit a355737

4 files changed

Lines changed: 183 additions & 52 deletions

File tree

packages/twenty-apps/internal/twenty-partners/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "twenty-partners",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"license": "MIT",
55
"engines": {
66
"node": "^24.5.0",
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { CoreApiClient } from 'twenty-client-sdk/core';
2+
import { defineLogicFunction } from 'twenty-sdk/define';
3+
4+
export const GET_PARTNER_BY_SLUG_LOGIC_FUNCTION_ID =
5+
'5e3e7b88-2cf2-4f56-9a4a-46c4c1d6b0bb';
6+
7+
type CurrencyValue = { amountMicros: number; currencyCode: string } | null;
8+
type LinkValue = { primaryLinkUrl: string | null } | null;
9+
10+
type Partner = {
11+
id: string;
12+
name: string | null;
13+
slug: string | null;
14+
introduction: string | null;
15+
languagesSpoken: string[] | null;
16+
deploymentExpertise: string[] | null;
17+
partnerScope: string[] | null;
18+
region: string[] | null;
19+
calendarLink: LinkValue;
20+
hourlyRate: CurrencyValue;
21+
projectBudgetMin: CurrencyValue;
22+
projectBudgetTypical: CurrencyValue;
23+
linkedin: LinkValue;
24+
profilePicture: LinkValue;
25+
skills: string[] | null;
26+
city: string | null;
27+
country: string | null;
28+
};
29+
30+
type GetPartnerBySlugResult =
31+
| { ok: true; partner: Partner }
32+
| { ok: false; reason: 'NOT_FOUND' | string };
33+
34+
const handler = async (input: {
35+
queryStringParameters?: { slug?: string };
36+
}): Promise<GetPartnerBySlugResult> => {
37+
const slug = input?.queryStringParameters?.slug;
38+
if (typeof slug !== 'string' || slug.length === 0) {
39+
return { ok: false, reason: 'Missing slug query parameter' };
40+
}
41+
42+
try {
43+
const client = new CoreApiClient();
44+
45+
const result = await client.query({
46+
partners: {
47+
__args: {
48+
filter: {
49+
slug: { eq: slug },
50+
validationStage: { eq: 'VALIDATED' },
51+
availability: { eq: 'AVAILABLE' },
52+
},
53+
first: 1,
54+
},
55+
edges: {
56+
node: {
57+
id: true,
58+
name: true,
59+
slug: true,
60+
introduction: true,
61+
languagesSpoken: true,
62+
deploymentExpertise: true,
63+
partnerScope: true,
64+
region: true,
65+
calendarLink: { primaryLinkUrl: true },
66+
hourlyRate: { amountMicros: true, currencyCode: true },
67+
projectBudgetMin: { amountMicros: true, currencyCode: true },
68+
projectBudgetTypical: { amountMicros: true, currencyCode: true },
69+
linkedin: { primaryLinkUrl: true },
70+
profilePicture: { primaryLinkUrl: true },
71+
skills: true,
72+
city: true,
73+
country: true,
74+
},
75+
},
76+
},
77+
} as any);
78+
79+
const edges = (result?.partners?.edges ?? []) as Array<{ node: Partner }>;
80+
const partner = edges[0]?.node;
81+
82+
if (!partner) {
83+
return { ok: false, reason: 'NOT_FOUND' };
84+
}
85+
86+
return { ok: true, partner };
87+
} catch (err) {
88+
return {
89+
ok: false,
90+
reason: err instanceof Error ? err.message : String(err),
91+
};
92+
}
93+
};
94+
95+
export default defineLogicFunction({
96+
universalIdentifier: GET_PARTNER_BY_SLUG_LOGIC_FUNCTION_ID,
97+
name: 'get-partner-by-slug',
98+
description:
99+
'Returns a single VALIDATED + AVAILABLE partner by slug, or NOT_FOUND.',
100+
timeoutSeconds: 10,
101+
handler,
102+
httpRouteTriggerSettings: {
103+
path: '/partner-by-slug',
104+
httpMethod: 'GET',
105+
isAuthRequired: false,
106+
},
107+
});

packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,58 @@ import { defineLogicFunction } from 'twenty-sdk/define';
44
export const LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID =
55
'0f91164f-f492-41e8-9bb0-481be5a3d5b9';
66

7-
type Partner = {
8-
id: string;
9-
name: string | null;
10-
slug: string | null;
11-
introduction: string | null;
12-
languagesSpoken: string[] | null;
13-
deploymentExpertise: string[] | null;
14-
region: string[] | null;
15-
calendarLink: { primaryLinkUrl: string | null } | null;
16-
};
7+
// CoreApiClient is codegenerated from the synced workspace schema, so the query
8+
// selection is strictly typed. Keep the fetch in one place and derive the
9+
// response shape from it, so the HTTP contract can never drift from what we
10+
// actually ask the API for.
11+
const queryAvailablePartners = (client: CoreApiClient) =>
12+
client.query({
13+
partners: {
14+
__args: {
15+
filter: {
16+
validationStage: { eq: 'VALIDATED' },
17+
availability: { eq: 'AVAILABLE' },
18+
},
19+
orderBy: [{ name: 'AscNullsLast' }],
20+
first: 100,
21+
},
22+
edges: {
23+
node: {
24+
id: true,
25+
name: true,
26+
slug: true,
27+
introduction: true,
28+
languagesSpoken: true,
29+
deploymentExpertise: true,
30+
partnerScope: true,
31+
region: true,
32+
calendarLink: { primaryLinkUrl: true },
33+
hourlyRate: { amountMicros: true, currencyCode: true },
34+
projectBudgetMin: { amountMicros: true, currencyCode: true },
35+
projectBudgetTypical: { amountMicros: true, currencyCode: true },
36+
linkedin: { primaryLinkUrl: true },
37+
profilePicture: { primaryLinkUrl: true },
38+
skills: true,
39+
city: true,
40+
country: true,
41+
},
42+
},
43+
},
44+
});
45+
46+
type AvailablePartner = NonNullable<
47+
Awaited<ReturnType<typeof queryAvailablePartners>>['partners']
48+
>['edges'][number]['node'];
1749

1850
type ListAvailablePartnersResult =
19-
| { ok: true; count: number; partners: Partner[] }
51+
| { ok: true; count: number; partners: AvailablePartner[] }
2052
| { ok: false; reason: string };
2153

2254
const handler = async (): Promise<ListAvailablePartnersResult> => {
2355
try {
2456
const client = new CoreApiClient();
25-
26-
const result = await client.query({
27-
partners: {
28-
__args: {
29-
filter: {
30-
validationStage: { eq: 'VALIDATED' },
31-
availability: { eq: 'AVAILABLE' },
32-
},
33-
orderBy: [{ name: 'AscNullsLast' }],
34-
first: 100,
35-
},
36-
edges: {
37-
node: {
38-
id: true,
39-
name: true,
40-
slug: true,
41-
introduction: true,
42-
languagesSpoken: true,
43-
deploymentExpertise: true,
44-
region: true,
45-
calendarLink: { primaryLinkUrl: true },
46-
},
47-
},
48-
},
49-
} as any);
50-
51-
const partners = (
52-
(result?.partners?.edges ?? []) as Array<{ node: Partner }>
53-
).map((edge) => edge.node);
57+
const result = await queryAvailablePartners(client);
58+
const partners = (result.partners?.edges ?? []).map((edge) => edge.node);
5459

5560
return { ok: true, count: partners.length, partners };
5661
} catch (err) {

packages/twenty-apps/internal/twenty-partners/src/scripts/seed.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ const requireEnv = (name: string): string => {
2222
};
2323

2424
const CAL = 'https://calendly.com/placeholder';
25+
const usd = (dollars: number) => ({
26+
amountMicros: dollars * 1_000_000,
27+
currencyCode: 'USD',
28+
});
29+
// Deterministic placeholder avatars; pravatar returns a stable image per `u` seed.
30+
const avatar = (slug: string) => `https://i.pravatar.cc/300?u=${slug}`;
31+
const linkedin = (slug: string) =>
32+
`https://www.linkedin.com/company/${slug}`;
2533

2634
type Partner = {
2735
slug: string;
@@ -37,19 +45,24 @@ type Partner = {
3745
partnerScope: string[];
3846
typeOfTeam: string;
3947
country: string;
48+
city: string;
49+
hourlyRateUsd: number | null;
50+
projectBudgetMinUsd: number | null;
51+
projectBudgetTypicalUsd: number | null;
52+
skills: string[];
4053
};
4154

4255
const PARTNERS: Partner[] = [
43-
{ slug: 'nine-dots-ventures', name: 'Nine Dots Ventures', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Boutique CRM implementer for real-estate workflows and WhatsApp automation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['EUROPE', 'MENA'], languagesSpoken: ['ENGLISH', 'FRENCH', 'ARABIC'], partnerTier: 'ADVANCED', partnerScope: ['DATA_MODEL', 'WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'FRANCE' },
44-
{ slug: 'elevate-consulting', name: 'Elevate Consulting', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Revenue-operations partner for B2B SaaS teams scaling seed to Series C.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['US', 'LATAM'], languagesSpoken: ['ENGLISH', 'SPANISH'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MIGRATION', 'DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' },
45-
{ slug: 'w3villa-technologies', name: 'W3Villa Technologies', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Engineering-heavy partner running large self-hosted Twenty deployments.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'MENA'], languagesSpoken: ['ENGLISH', 'HINDI'], partnerTier: 'ADVANCED', partnerScope: ['HOSTING_ENVIRONMENT', 'APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'INDIA' },
46-
{ slug: 'act-education', name: 'Act Education', validationStage: 'VALIDATED', availability: 'UNAVAILABLE', introduction: 'CRM partner for European education providers; compliance-first self-hosting.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'NEW', partnerScope: ['HOSTING_ENVIRONMENT', 'DATA_MODEL'], typeOfTeam: 'SOLO', country: 'GERMANY' },
47-
{ slug: 'netzero-systems', name: 'NetZero Systems', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'LATAM go-to-market partner for climate-tech and renewable-energy companies.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['LATAM', 'US'], languagesSpoken: ['ENGLISH', 'SPANISH', 'PORTUGUESE'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'BRAZIL' },
48-
{ slug: 'meridian-craft', name: 'Meridian Craft', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'APAC implementation studio for fintech and logistics; English + Chinese.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'AFRICA'], languagesSpoken: ['ENGLISH', 'CHINESE', 'MALAY'], partnerTier: 'ADVANCED', partnerScope: ['APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'SINGAPORE' },
49-
{ slug: 'applicant-studio', name: 'Applicant Studio', validationStage: 'APPLICATION', availability: 'UNAVAILABLE', introduction: 'New applicant; awaiting first review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'FRENCH'], partnerTier: 'NEW', partnerScope: ['DATA_MODEL'], typeOfTeam: 'SOLO', country: 'FRANCE' },
50-
{ slug: 'rising-crm', name: 'Rising CRM', validationStage: 'POTENTIAL', availability: 'AVAILABLE', introduction: 'Promising applicant in evaluation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['US'], languagesSpoken: ['ENGLISH'], partnerTier: 'NEW', partnerScope: ['WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' },
51-
{ slug: 'legacy-partners', name: 'Legacy Partners', validationStage: 'FORMER', availability: 'UNAVAILABLE', introduction: 'Former partner; no longer active in the program.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'INTERMEDIATE', partnerScope: ['HOSTING_ENVIRONMENT'], typeOfTeam: 'AGENCY', country: 'UNITED_KINGDOM' },
52-
{ slug: 'declined-co', name: 'Declined Co', validationStage: 'REJECTED', availability: 'UNAVAILABLE', introduction: 'Application rejected after review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['MENA'], languagesSpoken: ['ENGLISH', 'ARABIC'], partnerTier: 'NEW', partnerScope: ['APPS'], typeOfTeam: 'SOLO', country: 'UNITED_ARAB_EMIRATES' },
56+
{ slug: 'nine-dots-ventures', name: 'Nine Dots Ventures', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Boutique CRM implementer for real-estate workflows and WhatsApp automation. Nine Dots runs end-to-end Twenty rollouts for property managers and brokerages across Europe and MENA, with deep multi-language data models and AI-assisted lead intake.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['EUROPE', 'MENA'], languagesSpoken: ['ENGLISH', 'FRENCH', 'ARABIC'], partnerTier: 'ADVANCED', partnerScope: ['DATA_MODEL', 'WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'FRANCE', city: 'Paris', hourlyRateUsd: 250, projectBudgetMinUsd: 15000, projectBudgetTypicalUsd: 80000, skills: ['Real estate', 'WhatsApp', 'Multi-language', 'Workflows', 'Integrations', 'AI'] },
57+
{ slug: 'elevate-consulting', name: 'Elevate Consulting', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Revenue-operations partner for B2B SaaS teams scaling seed to Series C. Elevate moves teams off legacy CRMs onto Twenty with a four-week migration playbook, pipeline rebuilds, and analytics handoff.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['US', 'LATAM'], languagesSpoken: ['ENGLISH', 'SPANISH'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MIGRATION', 'DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES', city: 'Austin', hourlyRateUsd: 200, projectBudgetMinUsd: 20000, projectBudgetTypicalUsd: 100000, skills: ['RevOps', 'B2B SaaS', 'Data migration', 'Pipelines', 'Salesforce migration', 'HubSpot migration'] },
58+
{ slug: 'w3villa-technologies', name: 'W3Villa Technologies', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Engineering-heavy partner running large self-hosted Twenty deployments. Specializes in hardened Kubernetes hosting, custom integrations, and 24/7 support contracts for regulated industries across APAC and the Gulf.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'MENA'], languagesSpoken: ['ENGLISH', 'HINDI'], partnerTier: 'ADVANCED', partnerScope: ['HOSTING_ENVIRONMENT', 'APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'INDIA', city: 'Bangalore', hourlyRateUsd: 120, projectBudgetMinUsd: 10000, projectBudgetTypicalUsd: 60000, skills: ['Self-hosting', 'Kubernetes', 'DevOps', 'Integrations', 'Workflows', 'Enterprise support'] },
59+
{ slug: 'act-education', name: 'Act Education', validationStage: 'VALIDATED', availability: 'UNAVAILABLE', introduction: 'CRM partner for European education providers; compliance-first self-hosting on EU infrastructure with full GDPR data residency and student-record workflows.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'NEW', partnerScope: ['HOSTING_ENVIRONMENT', 'DATA_MODEL'], typeOfTeam: 'SOLO', country: 'GERMANY', city: 'Berlin', hourlyRateUsd: 180, projectBudgetMinUsd: 8000, projectBudgetTypicalUsd: 40000, skills: ['Education', 'Compliance', 'Self-hosting', 'GDPR', 'Data privacy'] },
60+
{ slug: 'netzero-systems', name: 'NetZero Systems', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'LATAM go-to-market partner for climate-tech and renewable-energy companies. Builds bilingual sales pipelines, ESG reporting, and grant-management workflows on top of Twenty.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['LATAM', 'US'], languagesSpoken: ['ENGLISH', 'SPANISH', 'PORTUGUESE'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'BRAZIL', city: 'São Paulo', hourlyRateUsd: 150, projectBudgetMinUsd: 12000, projectBudgetTypicalUsd: 50000, skills: ['Climate tech', 'Renewable energy', 'ESG reporting', 'Bilingual pipelines', 'LATAM go-to-market'] },
61+
{ slug: 'meridian-craft', name: 'Meridian Craft', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'APAC implementation studio for fintech and logistics. Senior team of ex-bank engineers building high-throughput Twenty deployments across Singapore, Hong Kong, and Kuala Lumpur.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'AFRICA'], languagesSpoken: ['ENGLISH', 'CHINESE', 'MALAY'], partnerTier: 'ADVANCED', partnerScope: ['APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'SINGAPORE', city: 'Singapore', hourlyRateUsd: 300, projectBudgetMinUsd: 25000, projectBudgetTypicalUsd: 120000, skills: ['Fintech', 'Logistics', 'APAC', 'High throughput', 'Custom apps', 'Performance tuning'] },
62+
{ slug: 'applicant-studio', name: 'Applicant Studio', validationStage: 'APPLICATION', availability: 'UNAVAILABLE', introduction: 'New applicant; awaiting first review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'FRENCH'], partnerTier: 'NEW', partnerScope: ['DATA_MODEL'], typeOfTeam: 'SOLO', country: 'FRANCE', city: 'Lyon', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['Boutique', 'Design'] },
63+
{ slug: 'rising-crm', name: 'Rising CRM', validationStage: 'POTENTIAL', availability: 'AVAILABLE', introduction: 'Promising applicant in evaluation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['US'], languagesSpoken: ['ENGLISH'], partnerTier: 'NEW', partnerScope: ['WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES', city: 'New York', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['SMB', 'Quick setup'] },
64+
{ slug: 'legacy-partners', name: 'Legacy Partners', validationStage: 'FORMER', availability: 'UNAVAILABLE', introduction: 'Former partner; no longer active in the program.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'INTERMEDIATE', partnerScope: ['HOSTING_ENVIRONMENT'], typeOfTeam: 'AGENCY', country: 'UNITED_KINGDOM', city: 'London', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['Enterprise', 'Self-hosting'] },
65+
{ slug: 'declined-co', name: 'Declined Co', validationStage: 'REJECTED', availability: 'UNAVAILABLE', introduction: 'Application rejected after review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['MENA'], languagesSpoken: ['ENGLISH', 'ARABIC'], partnerTier: 'NEW', partnerScope: ['APPS'], typeOfTeam: 'SOLO', country: 'UNITED_ARAB_EMIRATES', city: 'Dubai', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['MENA', 'Arabic'] },
5366
];
5467

5568
const COMPANIES = [
@@ -118,7 +131,13 @@ async function main() {
118131
introduction: p.introduction, calendarLink: { primaryLinkUrl: p.calendarLink },
119132
deploymentExpertise: p.deploymentExpertise, region: p.region, languagesSpoken: p.languagesSpoken,
120133
partnerTier: p.partnerTier, partnerScope: p.partnerScope, typeOfTeam: p.typeOfTeam,
121-
country: p.country,
134+
country: p.country, city: p.city,
135+
skills: p.skills,
136+
linkedin: { primaryLinkUrl: linkedin(p.slug) },
137+
profilePicture: { primaryLinkUrl: avatar(p.slug) },
138+
...(p.hourlyRateUsd != null ? { hourlyRate: usd(p.hourlyRateUsd) } : {}),
139+
...(p.projectBudgetMinUsd != null ? { projectBudgetMin: usd(p.projectBudgetMinUsd) } : {}),
140+
...(p.projectBudgetTypicalUsd != null ? { projectBudgetTypical: usd(p.projectBudgetTypicalUsd) } : {}),
122141
};
123142
const id = partnerIdBySlug.get(p.slug);
124143
if (id) {

0 commit comments

Comments
 (0)