Session 7h: Stripe products, tier config, scan limits, response gating, free tier

This commit is contained in:
Kev
2026-06-10 13:24:11 -04:00
parent 4e18eb1efe
commit d4e5e76452
16 changed files with 750 additions and 6 deletions
+81
View File
@@ -0,0 +1,81 @@
/**
* 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')
*
* 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.
*
* `api_access` is FALSE on every tier and must stay false. VYNDR is a
* consumer product; the proprietary engine is never exposed externally.
*
* `scans_per_day` is the per-tier daily limit. The scan-limit middleware
* reads this and 429s on overflow. Anonymous callers fall back to the
* 'free' bucket, IP-keyed.
*/
const TIERS = Object.freeze({
free: Object.freeze({
scans_per_day: 3,
grade_visible: true,
reasoning_visible: false, // blurred — frontend renders tier-locked
kill_conditions_detail: false, // count only, not codes/reasons
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,
reasoning_visible: true,
kill_conditions_detail: true,
kill_conditions_count: true,
alerts: true,
portfolio: false,
engine2: false,
stat_dashboard: true,
api_access: false,
}),
desk: Object.freeze({
scans_per_day: Infinity,
grade_visible: true,
reasoning_visible: true,
kill_conditions_detail: true,
kill_conditions_count: true,
alerts: true,
portfolio: true,
engine2: true, // LLM deep analysis
stat_dashboard: true,
api_access: false,
}),
});
const VALID_TIERS = Object.freeze(Object.keys(TIERS));
function getTier(tierName) {
const key = String(tierName || 'free').toLowerCase();
return TIERS[key] || TIERS.free;
}
function getScanLimit(tierName) {
return getTier(tierName).scans_per_day;
}
function canAccess(tierName, feature) {
const t = getTier(tierName);
if (!(feature in t)) return false;
return !!t[feature];
}
module.exports = {
TIERS,
VALID_TIERS,
getTier,
getScanLimit,
canAccess,
};