fix: create host records when team membership transitions to accepted via PATCH (#26138)

The PATCH endpoint for /v2/teams/:teamId/memberships/:membershipId had a bug
where host records for assignAllTeamMembers event types were not created when
a membership transitioned from accepted=false to accepted=true.

The bug was caused by incorrect operation ordering:
1. Update membership (first update)
2. Get current membership (gets already-updated state)
3. Update membership again (second update)
4. Check if transition happened (condition never fires)

Fixed by reordering to match the org-scoped controller pattern:
1. Get current membership BEFORE any updates
2. Update membership
3. Compare and call updateNewTeamMemberEventTypes if transition occurred

Also added an e2e test to verify host records are created when membership
transitions from accepted=false to accepted=true via PATCH.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom BalharaandDevin AI authored and GitHub committed 2025-12-23 10:46:14 +00:00
1 parent 96f8a89efa
commit 866c8c046d
2 files changed
+54 -3

No files matched your search

@@ -307,6 +307,57 @@ describe("Teams Memberships Endpoints", () => {
});
});
it("should assign team wide events when membership transitions from accepted=false to accepted=true via PATCH", async () => {
const pendingUserEmail = `pending-user-${randomString()}@api.com`;
const pendingUser = await userRepositoryFixture.create({
email: pendingUserEmail,
username: pendingUserEmail,
bio,
metadata,
});
const createPendingMembershipBody: CreateTeamMembershipInput = {
userId: pendingUser.id,
accepted: false,
role: "MEMBER",
};
const createResponse = await request(app.getHttpServer())
.post(`/v2/teams/${team.id}/memberships`)
.send(createPendingMembershipBody)
.expect(201);
const pendingMembership: TeamMembershipOutput = createResponse.body.data;
expect(pendingMembership.accepted).toEqual(false);
const teamEventTypesBeforePatch = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id);
const collectiveEventBeforePatch = teamEventTypesBeforePatch?.find(
(eventType) => eventType.slug === teamEventType.slug
);
const userHostBeforePatch = collectiveEventBeforePatch?.hosts.find((host) => host.userId === pendingUser.id);
expect(userHostBeforePatch).toBeFalsy();
const updateToAcceptedBody: UpdateTeamMembershipInput = {
accepted: true,
};
const patchResponse = await request(app.getHttpServer())
.patch(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
.send(updateToAcceptedBody)
.expect(200);
const acceptedMembership: TeamMembershipOutput = patchResponse.body.data;
expect(acceptedMembership.accepted).toEqual(true);
await userHasCorrectEventTypes(pendingUser.id);
await request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/memberships/${pendingMembership.id}`)
.expect(200);
await userRepositoryFixture.deleteByEmail(pendingUserEmail);
});
it("should delete the membership of the org's team we created via api", async () => {
return request(app.getHttpServer())
.delete(`/v2/teams/${team.id}/memberships/${membershipCreatedViaApi.id}`)
@@ -118,8 +118,6 @@ export class TeamsMembershipsController {
@Param("membershipId", ParseIntPipe) membershipId: number,
@Body() body: UpdateTeamMembershipInput
): Promise<UpdateTeamMembershipOutput> {
const membership = await this.teamsMembershipsService.updateTeamMembership(teamId, membershipId, body);
const currentMembership = await this.teamsMembershipsService.getTeamMembership(teamId, membershipId);
const updatedMembership = await this.teamsMembershipsService.updateTeamMembership(
@@ -127,6 +125,7 @@ export class TeamsMembershipsController {
membershipId,
body
);
if (!currentMembership.accepted && updatedMembership.accepted) {
try {
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
@@ -134,9 +133,10 @@ export class TeamsMembershipsController {
this.logger.error("Could not update new team member eventTypes", err);
}
}
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
data: plainToClass(TeamMembershipOutput, updatedMembership, { strategy: "excludeAll" }),
};
}