/*********************************************************************************************************************************************************************** * File Name: accessPolicy.util.js * Type of Program: Utility * Description: Evaluates a user's access to a course based on their active plan's access_rules JSONB. * * Rule types: * course_subscription_access — { type, levels: ['free','premium','exclusive'] } * → The plan grants access to these subscription levels only. * required_active_tier — { type, tier: 'premium' | 'exclusive' } * → The user's active tier must be at least this rank (exclusive satisfies premium). * group_restriction — { type, group_ids: [number, ...] } * → The user must belong to at least one of these groups. * * Fallback (no access_rules): uses simple tier rank comparison. ***********************************************************************************************************************************************************************/ 'use strict'; // Default rank map used as fallback when a live DB map is not available. // Overridden at call time with ranks loaded from tier_categories. const TIER_RANK = { free: 0, premium: 1, exclusive: 2 }; /** * Evaluates whether a user can access a course. * * @param {object} ctx * @param {string} ctx.tier — user's active tier slug * @param {Array} ctx.access_rules — plan_policies.access_rules (may be empty) * @param {number[]} ctx.group_ids — group IDs the user belongs to * @param {object} course * @param {string} course.subscription — course subscription level (slug) * @param {Object} tierRankMap — { [slug]: rank } loaded from tier_categories; falls back to TIER_RANK * @returns {{ allowed: boolean, reason: string|null }} */ function evaluateCourseAccess(ctx, course, tierRankMap = TIER_RANK) { const { tier = 'free', access_rules = [], group_ids = [] } = ctx; const courseSubscription = course.subscription ?? 'free'; const userRank = tierRankMap[tier] ?? 0; // Unknown required slug → Infinity so access is always denied (safe default) const courseRank = tierRankMap[courseSubscription] ?? Infinity; // Rank-0 courses (default/free tier) are always accessible if (courseRank === 0) return { allowed: true, reason: null }; // No plan policy — fallback: compare user rank vs course subscription rank if (!access_rules || access_rules.length === 0) { return userRank >= courseRank ? { allowed: true, reason: null } : { allowed: false, reason: 'tier_rank' }; } for (const rule of access_rules) { if (rule.type === 'course_subscription_access') { if (!(rule.levels ?? []).includes(courseSubscription)) { return { allowed: false, reason: 'subscription_access' }; } } if (rule.type === 'required_active_tier') { // Unknown rule tier slug → Infinity, so the rule always blocks const reqRank = tierRankMap[rule.tier] ?? Infinity; if (userRank < reqRank) { return { allowed: false, reason: 'required_tier' }; } } if (rule.type === 'group_restriction') { const required = (rule.group_ids ?? []).map(Number); if (required.length > 0) { const inGroup = required.some((gid) => group_ids.includes(gid)); if (!inGroup) return { allowed: false, reason: 'group_restriction' }; } } } return { allowed: true, reason: null }; } module.exports = { evaluateCourseAccess, TIER_RANK };