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)
docs/plans/
docs/brainstorms/
playground-pricing.html

View File

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

View File

@@ -186,16 +186,24 @@ function buildTiers(dodoPrices) {
if (monthlyEntry) {
const [monthlyId] = monthlyEntry;
const monthlyPrice = dodoPrices[monthlyId];
const priceCents = monthlyPrice?.priceCents ?? FALLBACK_PRICES[monthlyId];
if (priceCents != null) tier.monthlyPrice = priceCents / 100;
if (monthlyPrice) {
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;
}
if (annualEntry) {
const [annualId] = annualEntry;
const annualPrice = dodoPrices[annualId];
const priceCents = annualPrice?.priceCents ?? FALLBACK_PRICES[annualId];
if (priceCents != null) tier.annualPrice = priceCents / 100;
if (annualPrice) {
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;
}
@@ -239,9 +247,19 @@ export default async function handler(req) {
}
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 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
await setCache(result);

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -118,7 +118,7 @@
}
</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">
</head>
<body>