User leaving a group now clears their suggestions, invitations they sent & invitations the group received

This commit is contained in:
Kalle
2026-09-06 11:24:00 +03:00
parent e6515f3414
commit 9bd4e90abd
5 changed files with 163 additions and 1 deletions

View File

@@ -0,0 +1,13 @@
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
import { defineFactory } from "../core/defineFactory";
type InsertArgs = Parameters<typeof SQGroupRepository.insertLike>[0];
/** One group challenging another, sent by a member of the liker group. */
export const { create } = defineFactory({
insert: async (args: InsertArgs) => {
await SQGroupRepository.insertLike(args);
return args;
},
});

View File

@@ -0,0 +1,13 @@
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
import { defineFactory } from "../core/defineFactory";
type InsertArgs = Parameters<typeof SQGroupRepository.insertSuggestion>[0];
/** A group flagged as worth a look by one of the suggester group's own members. */
export const { create } = defineFactory({
insert: async (args: InsertArgs) => {
await SQGroupRepository.insertSuggestion(args);
return args;
},
});

View File

@@ -3,11 +3,14 @@ import { describe, expect, test } from "vitest";
import { backdate } from "~/db/seed/core/backdate";
import * as GroupMatchContinueVoteFactory from "~/db/seed/factories/GroupMatchContinueVoteFactory";
import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory";
import * as SQGroupLikeFactory from "~/db/seed/factories/SQGroupLikeFactory";
import * as SQGroupSuggestionFactory from "~/db/seed/factories/SQGroupSuggestionFactory";
import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory";
import * as TeamFactory from "~/db/seed/factories/TeamFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import { db } from "~/db/sql";
import * as GroupMatchContinueVoteRepository from "~/features/sendouq-match/GroupMatchContinueVoteRepository.server";
import invariant from "~/utils/invariant";
import { FULL_GROUP_SIZE } from "./q-constants";
import * as SQGroupRepository from "./SQGroupRepository.server";
@@ -392,4 +395,102 @@ describe("leaveGroup", () => {
expect(groupRow).toBeUndefined();
expect(await allChatRooms()).toHaveLength(0);
});
test("clears the challenges the group received", async () => {
const { ownGroup, challengerGroup } = await setupGroupWithChallenges();
await SQGroupRepository.leaveGroup(ownGroup.leaverId);
const likes = await SQGroupRepository.findAllLikesByGroupId(ownGroup.id);
expect(likes.received).toHaveLength(0);
expect(
await SQGroupRepository.findAllLikesByGroupId(challengerGroup.id),
).toMatchObject({ given: [] });
});
test("clears the challenges the leaver sent but not those of the members who stay", async () => {
const { ownGroup, leaverTargetGroup, stayerTargetGroup } =
await setupGroupWithChallenges();
await SQGroupRepository.leaveGroup(ownGroup.leaverId);
const likes = await SQGroupRepository.findAllLikesByGroupId(ownGroup.id);
expect(likes.given.map((like) => like.groupId)).toEqual([
stayerTargetGroup.id,
]);
expect(
await SQGroupRepository.findAllLikesByGroupId(leaverTargetGroup.id),
).toMatchObject({ received: [] });
});
test("clears the suggestions the leaver made but not those of the members who stay", async () => {
const { ownGroup, stayerTargetGroup } = await setupGroupWithChallenges();
await SQGroupRepository.leaveGroup(ownGroup.leaverId);
const suggestions = await SQGroupRepository.findAllSuggestionsByGroupId(
ownGroup.id,
);
expect(suggestions.map((suggestion) => suggestion.groupId)).toEqual([
stayerTargetGroup.id,
]);
});
});
/**
* A group of two whose first member is about to leave, with a challenge received, one sent by
* each of its members and a suggestion made by each of them.
*/
const setupGroupWithChallenges = async () => {
const [leaver, stayer] = await UserFactory.createMany(2);
invariant(leaver && stayer, "Expected two users");
const ownGroup = await SQGroupFactory.create({
memberUserIds: [leaver.id, stayer.id],
});
const [challengerGroup, leaverTargetGroup, stayerTargetGroup] =
await createSoloGroups(3);
await SQGroupLikeFactory.create({
likerGroupId: challengerGroup.id,
targetGroupId: ownGroup.id,
createdByUserId: challengerGroup.memberUserIds[0]!,
});
await SQGroupLikeFactory.create({
likerGroupId: ownGroup.id,
targetGroupId: leaverTargetGroup.id,
createdByUserId: leaver.id,
});
await SQGroupLikeFactory.create({
likerGroupId: ownGroup.id,
targetGroupId: stayerTargetGroup.id,
createdByUserId: stayer.id,
});
await SQGroupSuggestionFactory.create({
suggesterGroupId: ownGroup.id,
targetGroupId: challengerGroup.id,
createdByUserId: leaver.id,
});
await SQGroupSuggestionFactory.create({
suggesterGroupId: ownGroup.id,
targetGroupId: stayerTargetGroup.id,
createdByUserId: stayer.id,
});
return {
ownGroup: { ...ownGroup, leaverId: leaver.id },
challengerGroup,
leaverTargetGroup,
stayerTargetGroup,
};
};
const createSoloGroups = async (count: number) => {
const users = await UserFactory.createMany(count);
return Promise.all(
users.map((user) => SQGroupFactory.create({ memberUserIds: [user.id] })),
);
};

View File

@@ -325,6 +325,31 @@ export async function deleteLikesAndSuggestionsByGroupId(
await deleteSuggestionsByGroupId(groupId, trx);
}
/** Clears what the departing member is responsible for: every challenge the group received (the roster the other group challenged is gone) plus the challenges and suggestions that member made themselves. */
async function deleteLikesAndSuggestionsOnLeave(
{ groupId, userId }: { groupId: number; userId: number },
trx: Transaction<DB>,
) {
await trx
.deleteFrom("GroupLike")
.where((eb) =>
eb.or([
eb("GroupLike.targetGroupId", "=", groupId),
eb.and([
eb("GroupLike.likerGroupId", "=", groupId),
eb("GroupLike.createdByUserId", "=", userId),
]),
]),
)
.execute();
await trx
.deleteFrom("GroupSuggestion")
.where("GroupSuggestion.suggesterGroupId", "=", groupId)
.where("GroupSuggestion.createdByUserId", "=", userId)
.execute();
}
export function morphGroups({
survivingGroupId,
otherGroupId,
@@ -836,7 +861,7 @@ export function deleteAllLikesByGroupId(groupId: number) {
return db.transaction().execute((trx) => deleteLikesByGroupId(groupId, trx));
}
/** Removes the user from their group (deleting it if they were last). A ready check the group was in is called off; returns the ids of the groups that were in it. */
/** Removes the user from their group (deleting it if they were last). A ready check the group was in is called off; returns the ids of the groups that were in it. Challenges the group received and challenges/suggestions the leaver made are cleared. */
export function leaveGroup(userId: number) {
return db.transaction().execute(async (trx) => {
const userGroup = await trx
@@ -908,6 +933,11 @@ export function leaveGroup(userId: number) {
throw new SendouQError("Can't leave group when already in a match");
}
await deleteLikesAndSuggestionsOnLeave(
{ groupId: userGroup.id, userId },
trx,
);
await syncTeamId(userGroup.id, trx);
return { abortedReadyCheckGroupIds };

View File

@@ -0,0 +1,5 @@
---
navItem: sendouq
type: feature
---
User leaving a group now clears their suggestions, invitations they sent & invitations the group received