All files / server/src/features/program-item passedOverProgramItems.ts

88.67% Statements 47/53
78.57% Branches 22/28
100% Functions 16/16
88.67% Lines 47/53

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                                              60x         603x 603x     603x 456x             603x       200x     344x     603x   8743x                   603x 14x 14x       603x             60x     603x 589x     335x 205x       205x       14x   13x     14x 205x       205x   1184x   5x   1180x   1184x 1183x   1180x       14x 13x     201x 1x       1x 1x           1x 1x 1x 1x     1x                   1x 1x       1x     1x             1x    
import { MongoDbError } from "shared/types/api/errors";
import { EventLogAction } from "shared/types/models/eventLog";
import { ProgramItem } from "shared/types/models/programItem";
import { isLotterySignupProgramItem } from "shared/utils/isLotterySignupProgramItem";
import { Result, makeSuccessResult } from "shared/utils/result";
import {
  getDirectSignupPhaseStarted,
  getLotterySignupEnded,
} from "shared/utils/signupTimes";
import { getTimeNow } from "server/features/assignment/utils/getTimeNow";
import { DirectSignupsForProgramItem } from "server/features/direct-signup/directSignupTypes";
import { queueCancelledDeletedEmails } from "server/features/notifications/queueCancelledDeletedEmails";
import { addEventLogItems } from "server/features/user/event-log/eventLogRepository";
import {
  DeleteLotterySignupsParams,
  delLotterySignups,
} from "server/features/user/lottery-signup/lotterySignupRepository";
import { findUsers } from "server/features/user/userRepository";
import { logger } from "server/utils/logger";
 
// A lottery program item holding sign-ups goes to direct sign-up instead. Decided as the
// programme is saved rather than when a run reaches it, so the program item can say so up front,
// and the mark is what keeps the decision once those sign-ups are cancelled.
export const getPassedOverProgramItems = async (
  updatedProgramItems: readonly ProgramItem[],
  currentProgramItems: readonly ProgramItem[],
  directSignups: readonly DirectSignupsForProgramItem[],
): Promise<Result<ProgramItem[], MongoDbError>> => {
  const timeNowResult = await getTimeNow();
  Iif (!timeNowResult.ok) {
    return timeNowResult;
  }
  const attendeeCountByProgramItemId = new Map(
    directSignups.map((directSignup) => [
      directSignup.programItemId,
      directSignup.userSignups.length,
    ]),
  );
  // What is stored is what was there before this save, since neither field is in the import's
  // update - a program item the database does not know about is a new one and carries neither
  const decidedProgramItemIds = new Set(
    currentProgramItems
      .filter(
        (programItem) =>
          programItem.lotteryRanForStartTime !== undefined ||
          programItem.passedOverForLottery === true,
      )
      .map((programItem) => programItem.programItemId),
  );
 
  const passedOverProgramItems = updatedProgramItems.filter(
    (programItem) =>
      isLotterySignupProgramItem(programItem) &&
      !decidedProgramItemIds.has(programItem.programItemId) &&
      (attendeeCountByProgramItemId.get(programItem.programItemId) ?? 0) > 0 &&
      // Only while its schedule still has something to take away. Measured to the moment
      // direct sign-up would open rather than to the lottery's close, so a program item that
      // becomes a lottery one inside the phase gap does not have its open sign-up shut for
      // the rest of it.
      !getDirectSignupPhaseStarted(programItem, timeNowResult.value),
  );
 
  if (passedOverProgramItems.length > 0) {
    logger.info(
      `${passedOverProgramItems.length} program items already hold sign-ups, marking them as not taking part in a lottery: ${passedOverProgramItems.map((programItem) => programItem.programItemId).join(", ")}`,
    );
  }
 
  return makeSuccessResult(passedOverProgramItems);
};
 
// Lottery sign-ups for a program item that will not be lotteried after all. They are for a
// lottery that will not happen, so they go the same way as those for a program item whose program
// type leaves the lottery, and say the same thing. A sign-up whose lottery has already run is
// left alone, whatever happens to the program item afterwards.
export const removePassedOverLotterySignups = async (
  passedOverProgramItems: readonly ProgramItem[],
): Promise<Result<void, MongoDbError>> => {
  if (passedOverProgramItems.length === 0) {
    return makeSuccessResult();
  }
 
  const timeNowResult = await getTimeNow();
  Iif (!timeNowResult.ok) {
    return timeNowResult;
  }
 
  const passedOverProgramItemIds = new Set(
    passedOverProgramItems
      .filter(
        (programItem) =>
          !getLotterySignupEnded(programItem, timeNowResult.value),
      )
      .map((programItem) => programItem.programItemId),
  );
 
  const usersResult = await findUsers();
  Iif (!usersResult.ok) {
    return usersResult;
  }
 
  const usersToUpdate: DeleteLotterySignupsParams[] = usersResult.value.flatMap(
    (user) => {
      const lotterySignupProgramItemIds = user.lotterySignups
        .filter((lotterySignup) =>
          passedOverProgramItemIds.has(lotterySignup.programItemId),
        )
        .map((lotterySignup) => lotterySignup.programItemId);
 
      if (lotterySignupProgramItemIds.length === 0) {
        return [];
      }
      return { username: user.username, lotterySignupProgramItemIds };
    },
  );
 
  if (usersToUpdate.length === 0) {
    return makeSuccessResult();
  }
 
  const delLotterySignupsResult = await delLotterySignups(usersToUpdate);
  Iif (!delLotterySignupsResult.ok) {
    return delLotterySignupsResult;
  }
 
  const passedOverProgramItemsById = new Map(
    passedOverProgramItems.map((programItem) => [
      programItem.programItemId,
      programItem,
    ]),
  );
 
  const eventUpdates = usersToUpdate.flatMap((user) =>
    user.lotterySignupProgramItemIds.flatMap((programItemId) => {
      const programItem = passedOverProgramItemsById.get(programItemId);
      Iif (!programItem) {
        return [];
      }
      return {
        username: user.username,
        programItemId: programItem.programItemId,
        programItemStartTime: programItem.startTime,
        createdAt: new Date().toISOString(),
        action: EventLogAction.PROGRAM_ITEM_NO_LOTTERY_ANYMORE,
      };
    }),
  );
 
  const addEventLogItemsResult = await addEventLogItems(eventUpdates);
  Iif (!addEventLogItemsResult.ok) {
    return addEventLogItemsResult;
  }
 
  await queueCancelledDeletedEmails(
    eventUpdates,
    new Map(
      passedOverProgramItems.map((programItem) => [
        programItem.programItemId,
        programItem.title,
      ]),
    ),
  );
 
  return makeSuccessResult();
};