Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | 49x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 474x 28x 28x 86x 86x 28x 6x 6x 28x 6x 6x 28x 6x 4x 28x 4x 28x 205x 28x 1492x 28x 3x 4x 3x 3x 28x 28x 28x 28x | import { isBefore } from "date-fns";
import { countBy, first, groupBy } from "remeda";
import { config } from "shared/config";
import { MongoDbError } from "shared/types/api/errors";
import {
Result,
makeErrorResult,
makeSuccessResult,
} from "shared/utils/result";
import {
getLotteryRunTime,
getLotterySignupEndTime,
willNotBeLotteried,
} from "shared/utils/signupTimes";
import { getTimeNow } from "server/features/assignment/utils/getTimeNow";
import {
getValidLotterySignupProgramItems,
prepareAssignmentParams,
} from "server/features/assignment/utils/prepareAssignmentParams";
import { runAssignmentAlgorithm } from "server/features/assignment/utils/runAssignmentAlgorithm";
import { findDirectSignups } from "server/features/direct-signup/directSignupRepository";
import { getPopularity } from "server/features/program-item-popularity/getPopularity";
import {
findProgramItems,
saveProgramItemPopularity,
} from "server/features/program-item/programItemRepository";
import { findUsers } from "server/features/user/userRepository";
import { logger } from "server/utils/logger";
export const updateProgramItemPopularity = async (): Promise<
Result<void, MongoDbError>
> => {
logger.info("Calculate program item popularity");
const usersResult = await findUsers();
Iif (!usersResult.ok) {
return usersResult;
}
const programItemsResult = await findProgramItems();
Iif (!programItemsResult.ok) {
return programItemsResult;
}
const directSignupsResult = await findDirectSignups();
Iif (!directSignupsResult.ok) {
return directSignupsResult;
}
const { validLotterySignupsUsers, lotteryParticipantDirectSignups } =
prepareAssignmentParams(
usersResult.value,
programItemsResult.value,
directSignupsResult.value,
);
const validLotterySignupProgramItems = getValidLotterySignupProgramItems(
programItemsResult.value,
);
const timeNowResult = await getTimeNow();
Iif (!timeNowResult.ok) {
return timeNowResult;
}
// Simulating a program item no lottery will take would spend attendees' placements on it
// rather than on the ones a lottery is still coming for. Its stored figure stays as it is.
const lotteryProgramItems = validLotterySignupProgramItems.filter(
(programItem) => !willNotBeLotteried(programItem),
);
// Grouped by the lottery each program item belongs to rather than by its resolved start time
// as written: the simulation is handed these keys and matches items to the minute, so two
// groups inside one minute would simulate the same items twice and double their counts.
const programItemsByStartTimes = groupBy(
lotteryProgramItems,
getLotteryRunTime,
);
// Only start times whose lottery sign-up is still open. Popularity measures demand for the
// lottery, and once spots have been handed out the simulation no longer measures that -
// reduced capacity with every attendee still competing understates some items and inflates
// others. A start time is never simulated again, so a figure written then is kept for good.
const openLotteryStartTimes = Object.keys(programItemsByStartTimes).filter(
(startTime) => {
// Every program item in the group shares the start time the window is derived from,
// and a group is never empty, so first() gives one without an absent case to handle
const programItem = first(programItemsByStartTimes[startTime]);
return isBefore(
timeNowResult.value,
getLotterySignupEndTime(programItem),
);
},
);
const assignmentResults = openLotteryStartTimes.map((startTime) => {
const result = runAssignmentAlgorithm(
config.event().assignmentAlgorithm,
validLotterySignupsUsers,
lotteryProgramItems,
startTime,
lotteryParticipantDirectSignups,
programItemsResult.value,
);
return { result, startTime };
});
const successResults = assignmentResults.flatMap((assignmentResult) => {
Iif (!assignmentResult.result.ok) {
logger.error(
new Error(
`Popularity update: assignment for start time ${assignmentResult.startTime} failed: ${assignmentResult.result.error}`,
),
);
return [];
}
return assignmentResult.result.value;
});
const userAssignmentProgramIds = successResults
.flatMap((result) => result.results)
.map((result) => result.assignmentSignup.programItemId);
const programItemSignupsCounts = countBy(
userAssignmentProgramIds,
(id) => id,
);
const lotterySignups = validLotterySignupsUsers.flatMap(
(user) => user.lotterySignups,
);
const groupedLotterySignups = groupBy(
lotterySignups,
(signup) => signup.programItemId,
);
const programItemPopularityUpdates = Object.entries(
programItemSignupsCounts,
).flatMap(([programItemId, assignmentSignupCount]) => {
const programItem = lotteryProgramItems.find(
(item) => item.programItemId === programItemId,
);
Iif (!programItem) {
return [];
}
return {
programItemId,
popularity: getPopularity({
minAttendance: programItem.minAttendance,
maxAttendance: programItem.maxAttendance,
assignmentSignupCount,
lotterySignups: groupedLotterySignups[programItemId],
}),
};
});
const saveProgramItemPopularityResult = await saveProgramItemPopularity(
programItemPopularityUpdates,
);
Iif (!saveProgramItemPopularityResult.ok) {
return makeErrorResult(MongoDbError.UNKNOWN_ERROR);
}
logger.info("Program item popularity updated");
return makeSuccessResult();
};
|