fix(invite): saveAndFlush invalidation before INSERT + set invalidated_at on accept

- createInvite: use saveAndFlush when invalidating existing invite so the
  UPDATE is guaranteed to hit the DB before the new INSERT, preventing
  duplicate key violation on uq_household_invite_active
- acceptInvite: also set invalidated_at when marking invite as used, so
  accepted invites are fully removed from the partial unique index and
  cannot block future invite creation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 22:04:24 +02:00
parent 6aed303627
commit 44fd398701
3 changed files with 6 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ COPY src src
RUN ./mvnw package -DskipTests -B RUN ./mvnw package -DskipTests -B
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:21-jre-alpine
RUN apk add --no-cache libwebp
WORKDIR /app WORKDIR /app
COPY --from=build /app/target/*.jar app.jar COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080 EXPOSE 8080

View File

@@ -165,7 +165,7 @@ public class HouseholdService {
householdInviteRepository.findByHouseholdIdAndInvalidatedAtIsNull(household.getId()) householdInviteRepository.findByHouseholdIdAndInvalidatedAtIsNull(household.getId())
.ifPresent(existing -> { .ifPresent(existing -> {
existing.setInvalidatedAt(Instant.now()); existing.setInvalidatedAt(Instant.now());
householdInviteRepository.save(existing); householdInviteRepository.saveAndFlush(existing);
}); });
String code = generateInviteCode(); String code = generateInviteCode();
@@ -211,6 +211,7 @@ public class HouseholdService {
new UserAccount(email, name, passwordEncoder.encode(rawPassword))); new UserAccount(email, name, passwordEncoder.encode(rawPassword)));
invite.setStatus("used"); invite.setStatus("used");
invite.setInvalidatedAt(Instant.now());
householdInviteRepository.save(invite); householdInviteRepository.save(invite);
Household household = invite.getHousehold(); Household household = invite.getHousehold();

View File

@@ -507,11 +507,13 @@ class HouseholdServiceTest {
when(householdMemberRepository.findByUserEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(member)); when(householdMemberRepository.findByUserEmailIgnoreCase("sarah@example.com")).thenReturn(Optional.of(member));
when(householdInviteRepository.findByHouseholdIdAndInvalidatedAtIsNull(any())).thenReturn(Optional.of(existingInvite)); when(householdInviteRepository.findByHouseholdIdAndInvalidatedAtIsNull(any())).thenReturn(Optional.of(existingInvite));
when(householdInviteRepository.saveAndFlush(any(HouseholdInvite.class))).thenAnswer(i -> i.getArgument(0));
when(householdInviteRepository.save(any(HouseholdInvite.class))).thenAnswer(i -> i.getArgument(0)); when(householdInviteRepository.save(any(HouseholdInvite.class))).thenAnswer(i -> i.getArgument(0));
householdService.createInvite("sarah@example.com"); householdService.createInvite("sarah@example.com");
assertThat(existingInvite.getInvalidatedAt()).isNotNull(); assertThat(existingInvite.getInvalidatedAt()).isNotNull();
verify(householdInviteRepository, times(2)).save(any(HouseholdInvite.class)); verify(householdInviteRepository).saveAndFlush(existingInvite);
verify(householdInviteRepository).save(any(HouseholdInvite.class));
} }
} }