fix(catalog): update prices to match Dodo catalog via API (#2678)

* fix(catalog): update API Starter fallback prices to match Dodo

API Starter Monthly: $59.99 → $99.99
API Starter Annual: $490 → $999

* fix(catalog): log and expose priceSource (dodo/partial/fallback)

Console warns when fallback prices are used for individual products.
Response includes priceSource field: 'dodo' (all from API), 'partial'
(some failed), or 'fallback' (all failed). Makes silent failures
visible in Vercel logs and API response.

* fix(catalog): priceSource counts only public priced products, remove playground

priceSource was counting all CATALOG entries (including hidden
api_business and enterprise with no Dodo price), making it report
'partial' even when all visible prices came from Dodo.
Now counts only products rendered by buildTiers.
Removed playground-pricing.html from git.
This commit is contained in:
Elie Habib
2026-04-04 15:05:00 +04:00
committed by GitHub
parent 44bf58efc9
commit 5b2cc93560
7 changed files with 87 additions and 68 deletions

1
.gitignore vendored
View File

@@ -69,3 +69,4 @@ tmp/
# Local planning documents (not for public repo) # Local planning documents (not for public repo)
docs/plans/ docs/plans/
docs/brainstorms/ docs/brainstorms/
playground-pricing.html

View File

@@ -6,6 +6,6 @@
export const FALLBACK_PRICES = { export const FALLBACK_PRICES = {
'pdt_0Nbtt71uObulf7fGXhQup': 3999, // Pro Monthly 'pdt_0Nbtt71uObulf7fGXhQup': 3999, // Pro Monthly
'pdt_0NbttMIfjLWC10jHQWYgJ': 39999, // Pro Annual 'pdt_0NbttMIfjLWC10jHQWYgJ': 39999, // Pro Annual
'pdt_0NbttVmG1SERrxhygbbUq': 5999, // API Starter Monthly 'pdt_0NbttVmG1SERrxhygbbUq': 9999, // API Starter Monthly
'pdt_0Nbu2lawHYE3dv2THgSEV': 49000, // API Starter Annual 'pdt_0Nbu2lawHYE3dv2THgSEV': 99900, // API Starter Annual
}; };

View File

@@ -186,16 +186,24 @@ function buildTiers(dodoPrices) {
if (monthlyEntry) { if (monthlyEntry) {
const [monthlyId] = monthlyEntry; const [monthlyId] = monthlyEntry;
const monthlyPrice = dodoPrices[monthlyId]; const monthlyPrice = dodoPrices[monthlyId];
const priceCents = monthlyPrice?.priceCents ?? FALLBACK_PRICES[monthlyId]; if (monthlyPrice) {
if (priceCents != null) tier.monthlyPrice = priceCents / 100; tier.monthlyPrice = monthlyPrice.priceCents / 100;
} else if (FALLBACK_PRICES[monthlyId] != null) {
tier.monthlyPrice = FALLBACK_PRICES[monthlyId] / 100;
console.warn(`[product-catalog] FALLBACK price for ${monthlyId} ($${tier.monthlyPrice}) — Dodo fetch failed`);
}
tier.monthlyProductId = monthlyId; tier.monthlyProductId = monthlyId;
} }
if (annualEntry) { if (annualEntry) {
const [annualId] = annualEntry; const [annualId] = annualEntry;
const annualPrice = dodoPrices[annualId]; const annualPrice = dodoPrices[annualId];
const priceCents = annualPrice?.priceCents ?? FALLBACK_PRICES[annualId]; if (annualPrice) {
if (priceCents != null) tier.annualPrice = priceCents / 100; tier.annualPrice = annualPrice.priceCents / 100;
} else if (FALLBACK_PRICES[annualId] != null) {
tier.annualPrice = FALLBACK_PRICES[annualId] / 100;
console.warn(`[product-catalog] FALLBACK price for ${annualId} ($${tier.annualPrice}) — Dodo fetch failed`);
}
tier.annualProductId = annualId; tier.annualProductId = annualId;
} }
@@ -239,9 +247,19 @@ export default async function handler(req) {
} }
const dodoPrices = await fetchPricesFromDodo(); const dodoPrices = await fetchPricesFromDodo();
// Count only public priced products that buildTiers actually renders
const pricedPublicIds = Object.entries(CATALOG)
.filter(([, v]) => PUBLIC_TIER_GROUPS.includes(v.tierGroup) && v.tierGroup !== 'free' && v.tierGroup !== 'enterprise')
.map(([id]) => id);
const dodoPriceCount = pricedPublicIds.filter(id => dodoPrices[id]).length;
const expectedCount = pricedPublicIds.length;
const priceSource = dodoPriceCount === expectedCount ? 'dodo' : dodoPriceCount > 0 ? 'partial' : 'fallback';
if (priceSource !== 'dodo') {
console.warn(`[product-catalog] priceSource=${priceSource}: got ${dodoPriceCount}/${expectedCount} prices from Dodo`);
}
const tiers = buildTiers(dodoPrices); const tiers = buildTiers(dodoPrices);
const now = Date.now(); const now = Date.now();
const result = { tiers, fetchedAt: now, cachedUntil: now + CACHE_TTL * 1000 }; const result = { tiers, fetchedAt: now, cachedUntil: now + CACHE_TTL * 1000, priceSource };
// Cache the result // Cache the result
await setCache(result); await setCache(result);

View File

@@ -153,7 +153,7 @@ export const PRODUCT_CATALOG: Record<string, CatalogEntry> = {
dodoProductId: "pdt_0NbttVmG1SERrxhygbbUq", dodoProductId: "pdt_0NbttVmG1SERrxhygbbUq",
planKey: "api_starter", planKey: "api_starter",
displayName: "API Starter Monthly", displayName: "API Starter Monthly",
priceCents: 5999, priceCents: 9999,
billingPeriod: "monthly", billingPeriod: "monthly",
tierGroup: "api_starter", tierGroup: "api_starter",
features: API_STARTER_FEATURES, features: API_STARTER_FEATURES,
@@ -174,7 +174,7 @@ export const PRODUCT_CATALOG: Record<string, CatalogEntry> = {
dodoProductId: "pdt_0Nbu2lawHYE3dv2THgSEV", dodoProductId: "pdt_0Nbu2lawHYE3dv2THgSEV",
planKey: "api_starter_annual", planKey: "api_starter_annual",
displayName: "API Starter Annual", displayName: "API Starter Annual",
priceCents: 49000, priceCents: 99900,
billingPeriod: "annual", billingPeriod: "annual",
tierGroup: "api_starter", tierGroup: "api_starter",
features: API_STARTER_FEATURES, features: API_STARTER_FEATURES,

View File

@@ -34,8 +34,8 @@
}, },
{ {
"name": "API", "name": "API",
"monthlyPrice": 59.99, "monthlyPrice": 99.99,
"annualPrice": 490, "annualPrice": 999,
"description": "Programmatic access to intelligence data", "description": "Programmatic access to intelligence data",
"features": [ "features": [
"REST API access", "REST API access",

File diff suppressed because one or more lines are too long

View File

@@ -118,7 +118,7 @@
} }
</script> </script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
<script type="module" crossorigin src="/pro/assets/index-CGp6v6aT.js"></script> <script type="module" crossorigin src="/pro/assets/index-tPsb4iBC.js"></script>
<link rel="stylesheet" crossorigin href="/pro/assets/index-BGcnYE-e.css"> <link rel="stylesheet" crossorigin href="/pro/assets/index-BGcnYE-e.css">
</head> </head>
<body> <body>