Session 12: i18n (10 languages, cookie-based), Africa tier .99, locale switcher, RTL Arabic (1305 tests)

This commit is contained in:
Kev
2026-06-10 22:24:40 -04:00
parent e5c45ecc8e
commit d957dee17b
27 changed files with 1834 additions and 29 deletions
+26 -5
View File
@@ -1,12 +1,17 @@
/**
* Tier access matrix — single source of truth for what each tier unlocks.
*
* The tier set matches the DB CHECK constraint in migrations 001 + 011:
* tier IN ('free', 'analyst', 'desk')
* The tier set DOES NOT YET match the DB CHECK constraint. Sessions
* 001 + 011 wrote `tier IN ('free', 'analyst', 'desk')`. Session 12
* adds 'africa' here, but the constraint must be extended in a
* follow-up migration BEFORE the Stripe webhook can write 'africa'
* to users.tier — otherwise inserts will 23514 (check_violation).
*
* Free is the top of the funnel. Users see the grade (the hook) but the
* reasoning + kill-condition details stay locked. Analyst opens the
* intelligence. Desk adds engine2 (LLM) and portfolio tracking.
* Roadmap:
* 1. Manual SQL to extend the constraint (drop + re-add covering all
* four tiers) — out of scope for this session per no-migration rule.
* 2. Stripe webhook event for the africa price ID maps to tier='africa'.
* 3. The frontend Pricing component shows the tier (already done here).
*
* `api_access` is FALSE on every tier and must stay false. VYNDR is a
* consumer product; the proprietary engine is never exposed externally.
@@ -29,6 +34,22 @@ const TIERS = Object.freeze({
stat_dashboard: true,
api_access: false,
}),
// Session 12 — $4.99/mo Africa tier. Slotted between free and analyst.
// Same intelligence surface as analyst (reasoning + kill conditions
// visible) but lower scan budget; no alerts. Activation blocked on a
// DB CHECK constraint update — see header comment.
africa: Object.freeze({
scans_per_day: 10,
grade_visible: true,
reasoning_visible: true,
kill_conditions_detail: true,
kill_conditions_count: true,
alerts: false,
portfolio: false,
engine2: false,
stat_dashboard: true,
api_access: false,
}),
analyst: Object.freeze({
scans_per_day: 15,
grade_visible: true,