mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
revised: payment strategy
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -90,21 +90,32 @@ async function expireSession(session, passingScore) {
|
||||
return expiredAttempt;
|
||||
}
|
||||
|
||||
// Builds user context for evaluateCourseAccess: active tier slug, live tier
|
||||
// rank map, the active plan's access_rules (if any), and group memberships
|
||||
// (needed for the group_restriction rule type).
|
||||
// Builds user context for evaluateCourseAccess: the user's best (highest-rank)
|
||||
// active tier slug, live tier rank map, one ruleset per concurrently-active plan
|
||||
// that has access_rules configured, and group memberships (needed for the
|
||||
// group_restriction rule type). A user can hold more than one active tier at
|
||||
// once (e.g. premium + exclusive), so "tier" here is the effective best one for
|
||||
// plain rank checks, while "rulesets" preserves each active plan's own rules
|
||||
// for the OR-across-active-plans check in canAccessCourse.
|
||||
async function buildUserContext(user_id) {
|
||||
const activeTier = await getActiveTier(user_id);
|
||||
const tier = activeTier?.tier ?? 'free';
|
||||
const activeTiers = await getActiveTiers(user_id);
|
||||
|
||||
const categories = await mdl_TierCategories.findAll({ attributes: ['slug', 'rank'] });
|
||||
const tierRankMap = {};
|
||||
for (const c of categories) tierRankMap[c.slug] = c.rank;
|
||||
|
||||
let access_rules = [];
|
||||
if (activeTier?.plan_id) {
|
||||
const policy = await mdl_PlanPolicy.findOne({ where: { plan_id: activeTier.plan_id } });
|
||||
access_rules = policy?.access_rules ?? [];
|
||||
let tier = 'free';
|
||||
let bestRank = -Infinity;
|
||||
for (const t of activeTiers) {
|
||||
const rank = tierRankMap[t.tier] ?? 0;
|
||||
if (rank > bestRank) { bestRank = rank; tier = t.tier; }
|
||||
}
|
||||
|
||||
const rulesets = [];
|
||||
for (const t of activeTiers) {
|
||||
if (!t.plan_id) continue;
|
||||
const policy = await mdl_PlanPolicy.findOne({ where: { plan_id: t.plan_id } });
|
||||
if (policy?.access_rules?.length) rulesets.push({ tier: t.tier, access_rules: policy.access_rules });
|
||||
}
|
||||
|
||||
const memberships = await mdl_UserGroupMembers.findAll({
|
||||
@@ -113,7 +124,7 @@ async function buildUserContext(user_id) {
|
||||
});
|
||||
const group_ids = memberships.map((m) => m.group_id);
|
||||
|
||||
return { tier, tierRankMap, access_rules, group_ids };
|
||||
return { tier, tierRankMap, rulesets, group_ids, activeTiers };
|
||||
}
|
||||
|
||||
// ─── Shared tier + purchase access check ─────────────────────────────────────
|
||||
@@ -126,11 +137,25 @@ async function canAccessCourse(user_id, course_id) {
|
||||
|
||||
const userCtx = await buildUserContext(user_id);
|
||||
|
||||
// evaluateCourseAccess already falls back to plain rank comparison when the
|
||||
// active plan has no access_rules configured — same behavior as before for
|
||||
// every course/plan combination that hasn't opted into the richer engine.
|
||||
const { allowed } = evaluateCourseAccess(userCtx, course, userCtx.tierRankMap);
|
||||
if (allowed) return true;
|
||||
// Plain rank check against the user's best active tier — evaluateCourseAccess
|
||||
// falls back to rank comparison when access_rules is empty. Same behavior as
|
||||
// before for every course/plan combination that hasn't opted into the richer
|
||||
// rule engine.
|
||||
const { allowed: rankAllowed } = evaluateCourseAccess(
|
||||
{ tier: userCtx.tier, access_rules: [], group_ids: userCtx.group_ids },
|
||||
course, userCtx.tierRankMap
|
||||
);
|
||||
if (rankAllowed) return true;
|
||||
|
||||
// Rule-based: a user can hold more than one active plan concurrently, and each
|
||||
// one's access_rules is independent — access is granted if ANY of them allow it.
|
||||
for (const ruleset of userCtx.rulesets) {
|
||||
const { allowed } = evaluateCourseAccess(
|
||||
{ tier: ruleset.tier, access_rules: ruleset.access_rules, group_ids: userCtx.group_ids },
|
||||
course, userCtx.tierRankMap
|
||||
);
|
||||
if (allowed) return true;
|
||||
}
|
||||
|
||||
// Individual purchase as fallback
|
||||
const product = await mdl_Product.findOne({ where: { course_id } });
|
||||
@@ -161,7 +186,12 @@ async function canAccessUnit(user_id, unit_id) {
|
||||
const unit = await Unit.findOne({ where: { unit_id, ...notDeleted }, attributes: ['subscription'] });
|
||||
if (unit?.subscription) {
|
||||
const userCtx = await buildUserContext(user_id);
|
||||
const { allowed } = evaluateCourseAccess(userCtx, { subscription: unit.subscription }, userCtx.tierRankMap);
|
||||
// Unit-level subscription gating is a plain rank check against the user's
|
||||
// best active tier, not a rule-based one.
|
||||
const { allowed } = evaluateCourseAccess(
|
||||
{ tier: userCtx.tier, access_rules: [], group_ids: userCtx.group_ids },
|
||||
{ subscription: unit.subscription }, userCtx.tierRankMap
|
||||
);
|
||||
if (allowed) return true;
|
||||
}
|
||||
|
||||
@@ -216,9 +246,10 @@ function sanitizeQuestions(questions = []) {
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve the caller's active tier (returns null if free/expired)
|
||||
async function getActiveTier(user_id) {
|
||||
return mdl_UserTiers.findOne({
|
||||
// Resolve ALL of the caller's concurrently-active tiers (empty array if free/expired) —
|
||||
// a user can hold more than one active tier at once (e.g. premium + exclusive).
|
||||
async function getActiveTiers(user_id) {
|
||||
return mdl_UserTiers.findAll({
|
||||
where: { user_id, status: "active" },
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user