From 8df27e07b2016e55fd4d687e415f5085dda71e62 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Tue, 29 Mar 2022 15:57:05 +0100 Subject: [PATCH] Save my work this time... --- server/src/lib/targets/milestones.ts | 132 ++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/server/src/lib/targets/milestones.ts b/server/src/lib/targets/milestones.ts index f97d4374b..9794b4eae 100644 --- a/server/src/lib/targets/milestones.ts +++ b/server/src/lib/targets/milestones.ts @@ -1,4 +1,9 @@ -import { integer, MilestoneDocument } from "tachi-common"; +import db from "external/mongo/db"; +import CreateLogCtx from "lib/logger/logger"; +import { GoalSubscriptionDocument, integer, MilestoneDocument } from "tachi-common"; +import { EvaluatedGoalReturn, EvaluateGoalForUser } from "./goals"; + +const logger = CreateLogCtx(__filename); /** * Retrieves the goalID documents in a single array from the @@ -10,11 +15,42 @@ export function GetGoalIDsFromMilestone(milestone: MilestoneDocument) { return milestone.milestoneData.map((e) => e.goals.map((e) => e.goalID)).flat(1); } +/** + * Return all the goals inside this milestone. + */ +export async function GetGoalsInMilestone(milestone: MilestoneDocument) { + const goalIDs = GetGoalIDsFromMilestone(milestone); + + const goals = await db.goals.find({ + goalID: { $in: goalIDs }, + }); + + if (goals.length !== goalIDs.length) { + logger.error( + `Milestone ${milestone.name} has ${goalIDs.length} goals registered, but we could only find ${goals.length} in the database?`, + { goals: goals.length, milestone, goalIDs: goalIDs.length } + ); + throw new Error(`Milestone is corrupt. Not the right amount of goals in db?`); + } + + if (goalIDs.length < 2) { + logger.error( + `Milestone ${milestone.name} resolves to less than 2 goals. Isn't a valid milestone?`, + { milestone } + ); + throw new Error(`Milestone is corrupt. Doesn't have enough goals.`); + } + + return goals; +} + /** * Work out how many goals need to be achieved for this * milestone to be considered completed. */ -export function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: string[]): integer { +export function CalculateMilestoneOutOf(milestone: MilestoneDocument) { + const goalIDs = GetGoalIDsFromMilestone(milestone); + if (milestone.criteria.type === "all") { return goalIDs.length; } else if (milestone.criteria.type === "abs") { @@ -39,3 +75,95 @@ export function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: s `Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}` ); } + +type EvaluatedGoalResult = EvaluatedGoalReturn & { goalID: string }; + +/** + * Evaluate a user's progress on a milestone, regardless of whether they have it + * assigned or not. + * + * @returns All of the goals in the milestone. The users progress on each individual goal, + * their overall progress, what the milestone was outOf, and whether they achieved it or + * not. + */ +export async function EvaluateMilestoneProgress(userID: integer, milestone: MilestoneDocument) { + const goals = await GetGoalsInMilestone(milestone); + + const isSubscribedToMilestone = await db["milestone-subs"].findOne({ + milestoneID: milestone.milestoneID, + }); + + // If the user is subscribed the milestone, we don't need to calculate + // their progress on each goal. + const goalSubMap = new Map(); + if (isSubscribedToMilestone) { + const goalSubs = await db["goal-subs"].find({ + goalID: { $in: goals.map((e) => e.goalID) }, + userID, + }); + + for (const sub of goalSubs) { + goalSubMap.set(sub.goalID, sub); + } + } + + const results: EvaluatedGoalResult[] = await Promise.all( + goals.map(async (goal) => { + if (isSubscribedToMilestone) { + const goalSub = goalSubMap.get(goal.goalID); + + if (!goalSub) { + logger.error( + `User ${userID} has a corrupt subscription to milestone '${milestone.name}', They do not have all the goals in this milestone assigned.` + ); + + throw new Error( + `User has corrupt subscription to milestone. Cannot calculate.` + ); + } + + return { + achieved: goalSub.achieved, + progress: goalSub.progress, + outOf: goalSub.outOf, + progressHuman: goalSub.progressHuman, + outOfHuman: goalSub.outOfHuman, + goalID: goal.goalID, + }; + } + + const result = await EvaluateGoalForUser(goal, userID, logger); + + if (!result) { + logger.error( + `Failed to calculate ${userID} result for goal '${goal.title}'. Is the goal valid?`, + { goal, milestone } + ); + + throw new Error(`Goal inside milestone is corrupt.`); + } + + return { + achieved: result.achieved, + progress: result.progress, + outOf: result.outOf, + progressHuman: result.progressHuman, + outOfHuman: result.outOfHuman, + goalID: goal.goalID, + }; + }) + ); + + const progress = results.filter((e) => e.achieved).length; + const outOf = CalculateMilestoneOutOf(milestone); + + const achieved = progress >= outOf; + + return { + goals, + results, + achieved, + progress, + outOf, + }; +}