sendou.ink/app/features/plus-voting/actions/plus.voting.server.ts
2026-01-03 13:47:32 +02:00

91 lines
2.7 KiB
TypeScript

import type { ActionFunction } from "react-router";
import { requireUser } from "~/features/auth/core/user.server";
import type { PlusVoteFromFE } from "~/features/plus-voting/core";
import {
nextNonCompletedVoting,
rangeToMonthYear,
} from "~/features/plus-voting/core";
import { isVotingActive } from "~/features/plus-voting/core/voting-time";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { badRequestIfFalsy, parseRequestPayload } from "~/utils/remix.server";
import { PLUS_UPVOTE } from "../plus-voting-constants";
import { votingActionSchema } from "../plus-voting-schemas";
export const action: ActionFunction = async ({ request }) => {
const user = requireUser();
const data = await parseRequestPayload({
request,
schema: votingActionSchema,
});
if (!isVotingActive()) {
throw new Response(null, { status: 400 });
}
invariant(user.plusTier, "User should have plusTier");
const usersForVoting = await PlusVotingRepository.usersForVoting({
id: user.id,
plusTier: user.plusTier,
});
// this should not be needed but makes the voting a bit more resilient
// if there is a bug that causes some user to show up twice, or some user to show up who should not be included
const seen = new Set<number>();
const filteredVotes = data.votes.filter((vote) => {
if (seen.has(vote.votedId)) {
return false;
}
seen.add(vote.votedId);
return usersForVoting.some((u) => u.user.id === vote.votedId);
});
validateVotes({ votes: filteredVotes, usersForVoting });
// freebie +1 for yourself if you vote
const votesForDb = [...filteredVotes].concat({
votedId: user.id,
score: PLUS_UPVOTE,
});
const votingRange = badRequestIfFalsy(nextNonCompletedVoting(new Date()));
const { month, year } = rangeToMonthYear(votingRange);
await PlusVotingRepository.upsertMany(
votesForDb.map((vote) => ({
...vote,
authorId: user.id,
month,
year,
tier: user.plusTier!, // no clue why i couldn't make narrowing the type down above work
validAfter: dateToDatabaseTimestamp(votingRange.endDate),
})),
);
return null;
};
function validateVotes({
votes,
usersForVoting,
}: {
votes: PlusVoteFromFE[];
usersForVoting?: PlusVotingRepository.UsersForVoting;
}) {
if (!usersForVoting) throw new Response(null, { status: 400 });
// converting it to set also handles the check for duplicate ids
const votedUserIds = new Set(votes.map((v) => v.votedId));
if (votedUserIds.size !== usersForVoting.length) {
throw new Response(null, { status: 400 });
}
for (const { user } of usersForVoting) {
if (!votedUserIds.has(user.id)) {
throw new Response(null, { status: 400 });
}
}
}