Session 14: Africa checkout, Tank01 NBA/MLB wiring, WNBA+MLB odds proxies, OAuth icons, loading skeletons (1330 tests)
This commit is contained in:
@@ -12,8 +12,19 @@ const PRICE_MAP = {
|
||||
analyst_founder: process.env.STRIPE_PRICE_ANALYST_FOUNDER || 'price_analyst_founder',
|
||||
desk: process.env.STRIPE_PRICE_DESK || 'price_desk_monthly',
|
||||
desk_founder: process.env.STRIPE_PRICE_DESK_FOUNDER || 'price_desk_founder',
|
||||
// Session 14 — Africa tier ($4.99/mo). The Stripe product must be
|
||||
// created in the dashboard before STRIPE_PRICE_AFRICA carries a real
|
||||
// ID. Until then `getPriceId('africa')` returns a sentinel that
|
||||
// surfaces a clean error to the user via the route handler.
|
||||
africa: process.env.STRIPE_PRICE_AFRICA || null,
|
||||
};
|
||||
|
||||
// Sentinel marker — getPriceId returns this when the tier is valid
|
||||
// but the Stripe price hasn't been provisioned yet. The route layer
|
||||
// checks for it and returns 503 with a friendly message rather than
|
||||
// passing "null" to Stripe.
|
||||
const PRICE_UNCONFIGURED = '__unconfigured__';
|
||||
|
||||
// VYNDR is the canonical brand promo. BETONBLK stays in the default list so
|
||||
// codes distributed before the rebrand keep redeeming during the transition.
|
||||
const VALID_FOUNDER_CODES = (process.env.FOUNDER_CODES || 'FOUNDER2026,VYNDR,BETONBLK,EARLYBIRD').split(',');
|
||||
@@ -29,12 +40,30 @@ function getPriceId(tier, founderCode) {
|
||||
const isFounder = isFounderCodeValid(founderCode);
|
||||
if (tier === 'analyst') return isFounder ? PRICE_MAP.analyst_founder : PRICE_MAP.analyst;
|
||||
if (tier === 'desk') return isFounder ? PRICE_MAP.desk_founder : PRICE_MAP.desk;
|
||||
if (tier === 'africa') {
|
||||
// Africa tier doesn't have a founder discount — it IS the
|
||||
// discount. Returns the sentinel when STRIPE_PRICE_AFRICA is
|
||||
// unset so the route handler can produce a clean error instead
|
||||
// of forwarding a null price ID to Stripe.
|
||||
return PRICE_MAP.africa || PRICE_UNCONFIGURED;
|
||||
}
|
||||
throw new Error(`Invalid tier: ${tier}`);
|
||||
}
|
||||
|
||||
async function createCheckoutSession(userId, email, tier, founderCode) {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const priceId = getPriceId(tier, founderCode);
|
||||
// Session 14 — tier is valid but the upstream Stripe product
|
||||
// hasn't been provisioned (most common case: africa before
|
||||
// STRIPE_PRICE_AFRICA is configured in Coolify). Surface a clean
|
||||
// 503 with `code: 'tier_unconfigured'` instead of letting null
|
||||
// propagate to Stripe.
|
||||
if (priceId === PRICE_UNCONFIGURED) {
|
||||
const err = new Error(`Pricing for "${tier}" is not configured yet.`);
|
||||
err.code = 'tier_unconfigured';
|
||||
err.statusCode = 503;
|
||||
throw err;
|
||||
}
|
||||
const isFounder = isFounderCodeValid(founderCode);
|
||||
|
||||
// Get or create Stripe customer
|
||||
@@ -248,4 +277,7 @@ module.exports = {
|
||||
constructWebhookEvent,
|
||||
isFounderCodeValid,
|
||||
getPriceId,
|
||||
// Session 14 — exposed so the route layer + tests can recognize
|
||||
// the "tier valid but Stripe price not provisioned" state.
|
||||
PRICE_UNCONFIGURED,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user