Skip to content

Commit 10b8907

Browse files
open-design-crew[bot]elifive555555
andauthored
fix(landing): sync Team seat pricing with the $5 seat-fee catalog (#6834)
Vela #1263 dropped the Team seat fee to $5 and re-derived every Team tier off it, but /pricing kept quoting the old catalog — Team Pro read $120 / seat / month against a real $105, and the three-seat first-year total read $2,592 against a real $2,268. Anyone comparing the marketing page with the in-product upgrade modal saw two different prices. The seat prices are percentage-derived off the cloud catalog and no longer land on whole dollars ($73.50 first month, $100.63 on the yearly Max coupon), so `formatUsd` and its browser-side twin now render cents when an amount actually has them and keep whole dollars otherwise. The individual Creator tiers are untouched and still print as whole dollars. Prices come from db/seeds/local/seed-team-billing-catalog.py: tier list /seat/mo 1st month yearly /seat/mo 1st year Team Basic $5 $4 (-20%) $3.50 (-30%) $42 Team Plus $25 $20 (-20%) $17.50 (-30%) $210 Team Pro $105 $73.50(-30%) $63 (-40%) $756 Team Max $205 $123 (-40%) $100.63 (-51%) $1,207.61 pricing.md and the plans.json contract move with the snapshot, and the contract test's golden totals are re-derived from the new catalog. Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
1 parent 092a2fd commit 10b8907

5 files changed

Lines changed: 99 additions & 77 deletions

File tree

apps/landing-page/app/_lib/pricing.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,47 +159,59 @@ export const PRICING_SNAPSHOT: PricingContract = {
159159
recommended: false,
160160
minSeats: 3,
161161
monthlyCreditsPerSeatUsd: 0,
162-
monthly: { priceUsd: 20, introPriceUsd: 16 },
163-
yearly: { priceUsd: 240, introPriceUsd: 168, discountPct: 30 },
162+
monthly: { priceUsd: 5, introPriceUsd: 4 },
163+
yearly: { priceUsd: 60, introPriceUsd: 42, discountPct: 30 },
164164
},
165165
{
166166
tier: 'team_plus',
167167
rank: 1,
168168
recommended: false,
169169
minSeats: 3,
170170
monthlyCreditsPerSeatUsd: 20,
171-
monthly: { priceUsd: 40, introPriceUsd: 32 },
172-
yearly: { priceUsd: 480, introPriceUsd: 336, discountPct: 30 },
171+
monthly: { priceUsd: 25, introPriceUsd: 20 },
172+
yearly: { priceUsd: 300, introPriceUsd: 210, discountPct: 30 },
173173
},
174174
{
175175
tier: 'team_pro',
176176
rank: 2,
177177
recommended: true,
178178
minSeats: 3,
179179
monthlyCreditsPerSeatUsd: 100,
180-
monthly: { priceUsd: 120, introPriceUsd: 84 },
181-
yearly: { priceUsd: 1440, introPriceUsd: 864, discountPct: 40 },
180+
monthly: { priceUsd: 105, introPriceUsd: 73.5 },
181+
yearly: { priceUsd: 1260, introPriceUsd: 756, discountPct: 40 },
182182
},
183183
{
184184
tier: 'team_max',
185185
rank: 3,
186186
recommended: false,
187187
minSeats: 3,
188188
monthlyCreditsPerSeatUsd: 200,
189-
monthly: { priceUsd: 220, introPriceUsd: 132 },
190-
yearly: { priceUsd: 2640, introPriceUsd: 1296, discountPct: 51 },
189+
monthly: { priceUsd: 205, introPriceUsd: 123 },
190+
// The yearly Max coupon caps at 50.91%, so the first-year seat price does
191+
// not divide evenly into twelve months — keep the exact cloud amount.
192+
yearly: { priceUsd: 2460, introPriceUsd: 1207.61, discountPct: 51 },
191193
},
192194
],
193195
};
194196

195-
/** Whole-dollar USD, no trailing cents (prices are whole-dollar by design). */
197+
/**
198+
* USD with cents only when the amount actually has them. Individual plans are
199+
* whole-dollar, but Team seat prices are percentage-derived off the cloud
200+
* catalog and land on cents ($73.50 / seat / month, $100.63 / seat / month on
201+
* the yearly Max coupon), so silently rounding them would misquote the price.
202+
*/
196203
export function formatUsd(amount: number): string {
197-
return `$${Math.round(amount).toLocaleString('en-US')}`;
204+
const cents = Math.round(amount * 100);
205+
const value = cents / 100;
206+
return `$${value.toLocaleString('en-US', {
207+
minimumFractionDigits: cents % 100 === 0 ? 0 : 2,
208+
maximumFractionDigits: 2,
209+
})}`;
198210
}
199211

200-
/** Monthly-equivalent of an annual price, rounded to whole dollars. */
212+
/** Monthly-equivalent of an annual price, to the cent. */
201213
export function yearlyMonthlyEquivalent(yearlyPriceUsd: number): number {
202-
return Math.round(yearlyPriceUsd / 12);
214+
return Math.round((yearlyPriceUsd / 12) * 100) / 100;
203215
}
204216

205217
/** Introductory Team charge for the selected billing period and seat count. */

apps/landing-page/app/pages/pricing/index.astro

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
PLANS_JSON_URL,
1616
CLOUD_CONSOLE_URL,
1717
cloudSubscribeUrl,
18+
formatUsd,
1819
teamIntroTotalUsd,
1920
type PlanTierConfig,
2021
type TeamPlanTierConfig,
@@ -70,7 +71,7 @@ const LF = enterprise.leadForm;
7071
const SITE = Astro.site?.toString() ?? 'https://open-design.ai/';
7172
const title = teamContent.metaTitle;
7273
const description = teamContent.metaDescription;
73-
const usd = (n: number) => `$${Math.round(n).toLocaleString('en-US')}`;
74+
const usd = formatUsd;
7475
7576
const tiers: PlanTierConfig[] = PRICING_SNAPSHOT.tiers;
7677
const teamTiers: TeamPlanTierConfig[] = PRICING_SNAPSHOT.teamTiers;
@@ -93,11 +94,9 @@ function teamCreditOption(tier: TeamPlanTierConfig): string {
9394
function teamView(tier: TeamPlanTierConfig, interval: 'monthly' | 'yearly') {
9495
const selected = tier[interval];
9596
const months = interval === 'yearly' ? 12 : 1;
96-
const introSeatMonth = Math.round(selected.introPriceUsd / months);
97+
const introSeatMonth = selected.introPriceUsd / months;
9798
const regularSeatMonth =
98-
interval === 'yearly'
99-
? Math.round(tier.monthly.priceUsd)
100-
: Math.round(selected.priceUsd);
99+
interval === 'yearly' ? tier.monthly.priceUsd : selected.priceUsd;
101100
const discountPct = Math.round(
102101
(1 - introSeatMonth / regularSeatMonth) * 100,
103102
);
@@ -792,7 +791,18 @@ const jsonLd = [
792791
}
793792
return target.toString();
794793
};
795-
const usd = (n) => '$' + Math.round(n).toLocaleString('en-US');
794+
// Mirrors `formatUsd` in _lib/pricing.ts: cents only when the amount has
795+
// them, so Team seat prices like $73.50 are not rounded into a wrong quote.
796+
const usd = (n) => {
797+
const cents = Math.round(n * 100);
798+
return (
799+
'$' +
800+
(cents / 100).toLocaleString('en-US', {
801+
minimumFractionDigits: cents % 100 === 0 ? 0 : 2,
802+
maximumFractionDigits: 2,
803+
})
804+
);
805+
};
796806
const fill = (tpl, vals) =>
797807
tpl.replace(/\{(\w+)\}/g, (_, k) => (k in vals ? vals[k] : '{' + k + '}'));
798808
const set = (scope, sel, text) => {
@@ -981,13 +991,13 @@ const jsonLd = [
981991
const interval = root.getAttribute('data-interval') === 'monthly' ? 'monthly' : 'yearly';
982992
const selected = tier[interval];
983993
const months = interval === 'yearly' ? 12 : 1;
984-
const introSeatMonth = Math.round(selected.introPriceUsd / months);
994+
const introSeatMonth = selected.introPriceUsd / months;
985995
const listSeatMonth = tier.monthly.priceUsd;
986996
const discountPct = Math.round((1 - introSeatMonth / listSeatMonth) * 100);
987997
const intervalTotal = selected.introPriceUsd * teamSeats;
988-
const amount = '$' + introSeatMonth.toLocaleString('en-US');
989-
const strike = '$' + listSeatMonth.toLocaleString('en-US');
990-
const total = '$' + intervalTotal.toLocaleString('en-US');
998+
const amount = usd(introSeatMonth);
999+
const strike = usd(listSeatMonth);
1000+
const total = usd(intervalTotal);
9911001
const introTotalTemplate =
9921002
interval === 'yearly' ? teamCopy.yearlyTotal : teamCopy.monthlyTotal;
9931003

apps/landing-page/public/pricing.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,29 @@ not required to use Open Design.
3939
- For: heavy users and teams
4040

4141
## Team Basic
42-
- Standard price: **$20 / seat / month** or **$240 / seat / year**
43-
- Intro price: **$16 / seat / month** or **$168 / seat / year**
42+
- Standard price: **$5 / seat / month** or **$60 / seat / year**
43+
- Intro price: **$4 / seat / month** or **$42 / seat / year**
4444
- Included model usage credits: none
4545
- Minimum: 3 seats
4646
- For: shared projects, design systems, and plugins without bundled model credits
4747

4848
## Team Plus
49-
- Standard price: **$40 / seat / month** or **$480 / seat / year**
50-
- Intro price: **$32 / seat / month** or **$336 / seat / year**
49+
- Standard price: **$25 / seat / month** or **$300 / seat / year**
50+
- Intro price: **$20 / seat / month** or **$210 / seat / year**
5151
- Included model usage credits: $20 / seat / month
5252
- Minimum: 3 seats
5353
- For: small teams that want bundled model credits
5454

5555
## Team Pro — recommended
56-
- Standard price: **$120 / seat / month** or **$1,440 / seat / year**
57-
- Intro price: **$84 / seat / month** or **$864 / seat / year**
56+
- Standard price: **$105 / seat / month** or **$1,260 / seat / year**
57+
- Intro price: **$73.50 / seat / month** or **$756 / seat / year**
5858
- Included model usage credits: $100 / seat / month
5959
- Minimum: 3 seats
6060
- For: teams collaborating on regular production work
6161

6262
## Team Max
63-
- Standard price: **$220 / seat / month** or **$2,640 / seat / year**
64-
- Intro price: **$132 / seat / month** or **$1,296 / seat / year**
63+
- Standard price: **$205 / seat / month** or **$2,460 / seat / year**
64+
- Intro price: **$123 / seat / month** or **$1,207.61 / seat / year**
6565
- Included model usage credits: $200 / seat / month
6666
- Minimum: 3 seats
6767
- For: high-volume teams that need the largest bundled allowance

apps/landing-page/public/pricing/plans.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@
6060
"minSeats": 3,
6161
"monthlyCreditsPerSeatUsd": 0,
6262
"monthly": {
63-
"priceUsd": 20,
64-
"introPriceUsd": 16
63+
"priceUsd": 5,
64+
"introPriceUsd": 4
6565
},
6666
"yearly": {
67-
"priceUsd": 240,
68-
"introPriceUsd": 168,
67+
"priceUsd": 60,
68+
"introPriceUsd": 42,
6969
"discountPct": 30
7070
}
7171
},
@@ -76,12 +76,12 @@
7676
"minSeats": 3,
7777
"monthlyCreditsPerSeatUsd": 20,
7878
"monthly": {
79-
"priceUsd": 40,
80-
"introPriceUsd": 32
79+
"priceUsd": 25,
80+
"introPriceUsd": 20
8181
},
8282
"yearly": {
83-
"priceUsd": 480,
84-
"introPriceUsd": 336,
83+
"priceUsd": 300,
84+
"introPriceUsd": 210,
8585
"discountPct": 30
8686
}
8787
},
@@ -92,12 +92,12 @@
9292
"minSeats": 3,
9393
"monthlyCreditsPerSeatUsd": 100,
9494
"monthly": {
95-
"priceUsd": 120,
96-
"introPriceUsd": 84
95+
"priceUsd": 105,
96+
"introPriceUsd": 73.5
9797
},
9898
"yearly": {
99-
"priceUsd": 1440,
100-
"introPriceUsd": 864,
99+
"priceUsd": 1260,
100+
"introPriceUsd": 756,
101101
"discountPct": 40
102102
}
103103
},
@@ -108,12 +108,12 @@
108108
"minSeats": 3,
109109
"monthlyCreditsPerSeatUsd": 200,
110110
"monthly": {
111-
"priceUsd": 220,
112-
"introPriceUsd": 132
111+
"priceUsd": 205,
112+
"introPriceUsd": 123
113113
},
114114
"yearly": {
115-
"priceUsd": 2640,
116-
"introPriceUsd": 1296,
115+
"priceUsd": 2460,
116+
"introPriceUsd": 1207.61,
117117
"discountPct": 51
118118
}
119119
}

apps/landing-page/tests/pricing-contract.test.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -272,37 +272,37 @@ describe("pricing contract", () => {
272272
[
273273
{
274274
tier: "team_basic",
275-
monthly: 20,
276-
monthlyIntro: 16,
277-
yearly: 240,
278-
yearlyIntro: 168,
275+
monthly: 5,
276+
monthlyIntro: 4,
277+
yearly: 60,
278+
yearlyIntro: 42,
279279
credits: 0,
280280
minSeats: 3,
281281
},
282282
{
283283
tier: "team_plus",
284-
monthly: 40,
285-
monthlyIntro: 32,
286-
yearly: 480,
287-
yearlyIntro: 336,
284+
monthly: 25,
285+
monthlyIntro: 20,
286+
yearly: 300,
287+
yearlyIntro: 210,
288288
credits: 20,
289289
minSeats: 3,
290290
},
291291
{
292292
tier: "team_pro",
293-
monthly: 120,
294-
monthlyIntro: 84,
295-
yearly: 1440,
296-
yearlyIntro: 864,
293+
monthly: 105,
294+
monthlyIntro: 73.5,
295+
yearly: 1260,
296+
yearlyIntro: 756,
297297
credits: 100,
298298
minSeats: 3,
299299
},
300300
{
301301
tier: "team_max",
302-
monthly: 220,
303-
monthlyIntro: 132,
304-
yearly: 2640,
305-
yearlyIntro: 1296,
302+
monthly: 205,
303+
monthlyIntro: 123,
304+
yearly: 2460,
305+
yearlyIntro: 1207.61,
306306
credits: 200,
307307
minSeats: 3,
308308
},
@@ -313,28 +313,28 @@ describe("pricing contract", () => {
313313
it("renders all 16 introductory Team totals for interval, tier, and seat changes", () => {
314314
const expected = {
315315
team_basic: {
316-
monthly: { 3: "First month only $48", 4: "First month only $64" },
317-
yearly: { 3: "First year only $504", 4: "First year only $672" },
316+
monthly: { 3: "First month only $12", 4: "First month only $16" },
317+
yearly: { 3: "First year only $126", 4: "First year only $168" },
318318
},
319319
team_plus: {
320-
monthly: { 3: "First month only $96", 4: "First month only $128" },
321-
yearly: {
322-
3: "First year only $1,008",
323-
4: "First year only $1,344",
324-
},
320+
monthly: { 3: "First month only $60", 4: "First month only $80" },
321+
yearly: { 3: "First year only $630", 4: "First year only $840" },
325322
},
326323
team_pro: {
327-
monthly: { 3: "First month only $252", 4: "First month only $336" },
324+
monthly: {
325+
3: "First month only $220.50",
326+
4: "First month only $294",
327+
},
328328
yearly: {
329-
3: "First year only $2,592",
330-
4: "First year only $3,456",
329+
3: "First year only $2,268",
330+
4: "First year only $3,024",
331331
},
332332
},
333333
team_max: {
334-
monthly: { 3: "First month only $396", 4: "First month only $528" },
334+
monthly: { 3: "First month only $369", 4: "First month only $492" },
335335
yearly: {
336-
3: "First year only $3,888",
337-
4: "First year only $5,184",
336+
3: "First year only $3,622.83",
337+
4: "First year only $4,830.44",
338338
},
339339
},
340340
} as const;
@@ -541,11 +541,11 @@ describe("pricing contract", () => {
541541
.replace(/\b\w/g, (character) => character.toUpperCase());
542542
assert.ok(md.includes(`## ${label}`), `pricing.md missing ${label}`);
543543
assert.ok(
544-
md.includes(`$${tier.monthly.introPriceUsd.toLocaleString("en-US")} / seat / month`),
544+
md.includes(`${formatUsd(tier.monthly.introPriceUsd)} / seat / month`),
545545
`pricing.md missing ${label} monthly intro price`,
546546
);
547547
assert.ok(
548-
md.includes(`$${tier.yearly.introPriceUsd.toLocaleString("en-US")} / seat / year`),
548+
md.includes(`${formatUsd(tier.yearly.introPriceUsd)} / seat / year`),
549549
`pricing.md missing ${label} yearly intro price`,
550550
);
551551
}

0 commit comments

Comments
 (0)