Session 14: Africa checkout, Tank01 NBA/MLB wiring, WNBA+MLB odds proxies, OAuth icons, loading skeletons (1330 tests)

This commit is contained in:
Kev
2026-06-11 10:06:49 -04:00
parent 10159209fa
commit f5d79cf70d
22 changed files with 979 additions and 27 deletions
+33
View File
@@ -108,6 +108,39 @@ router.get('/nba', async (req, res) => {
}
});
// Session 14 — WNBA + MLB. Same pattern as /nba: validate query,
// fetch via cached oddsService, project to the {sport, props}
// envelope the Slate consumes. odds-api may return empty during
// off-season; we still return 200 with an empty `props` array so
// the Slate can render its empty-state UX.
function buildSportRoute(sport) {
return async (req, res) => {
const errors = validateQueryParams(req.query);
if (errors.length > 0) {
return res.status(400).json({ error: errors.join('; ') });
}
try {
const result = await getOdds(sport);
const filtered = filterProps(result.props || [], req.query);
const props = groupProps(filtered);
if (result.stale) res.set('X-VYNDR-Stale', 'true');
return res.json({
sport,
updated_at: result.updated_at,
source: result.source,
quota_remaining: result.quota_remaining,
props,
});
} catch (err) {
const status = err.statusCode || 500;
return res.status(status).json({ error: err.message });
}
};
}
router.get('/wnba', buildSportRoute('wnba'));
router.get('/mlb', buildSportRoute('mlb'));
router.get('/ncaab', async (req, res) => {
if (!isNcaabSeason()) {
return res.json({
+16 -2
View File
@@ -14,8 +14,12 @@ const router = express.Router();
router.post('/checkout', requireAuth, async (req, res) => {
const { tier, founder_code } = req.body;
if (!tier || !['analyst', 'desk'].includes(tier)) {
return res.status(400).json({ error: 'tier must be "analyst" or "desk"' });
// Session 14 — 'africa' joins the validation whitelist. Whether
// the checkout succeeds for 'africa' depends on STRIPE_PRICE_AFRICA
// being set (see stripeService.PRICE_UNCONFIGURED handling); when
// it isn't, the service throws a 503 the catch block surfaces.
if (!tier || !['africa', 'analyst', 'desk'].includes(tier)) {
return res.status(400).json({ error: 'tier must be "africa", "analyst" or "desk"' });
}
try {
@@ -23,6 +27,16 @@ router.post('/checkout', requireAuth, async (req, res) => {
return res.json(result);
} catch (err) {
console.error('[VYNDR] Checkout error:', err.message);
// Session 14 — surface the "tier valid but Stripe price not
// provisioned yet" case with the explicit message + 503. This
// path is what the Africa-tier user hits until
// STRIPE_PRICE_AFRICA is configured in Coolify.
if (err && err.code === 'tier_unconfigured') {
return res.status(503).json({
error: err.message || 'Tier pricing not configured yet.',
code: 'tier_unconfigured',
});
}
return res.status(503).json({ error: 'Checkout creation failed' });
}
});