fix(journeyitem): translate only the dedup-index violation to 409
Any other DataIntegrityViolationException at flush (e.g. an FK violation on a concurrently deleted document) is rethrown instead of being mislabeled as JOURNEY_DOCUMENT_ALREADY_ADDED. Match on the uq_journey_items_geschichte_document constraint name. Review round 3: Markus (a), Felix suggestion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,12 @@ public class JourneyItemService {
|
|||||||
try {
|
try {
|
||||||
saved = journeyItemRepository.saveAndFlush(item);
|
saved = journeyItemRepository.saveAndFlush(item);
|
||||||
} catch (DataIntegrityViolationException e) {
|
} catch (DataIntegrityViolationException e) {
|
||||||
|
// Only the dedup index earns the friendly 409 — any other integrity
|
||||||
|
// failure (e.g. an FK violation on a concurrently deleted document)
|
||||||
|
// must not be mislabeled as "already added".
|
||||||
|
if (!isDuplicateDocumentViolation(e)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
throw DomainException.conflict(ErrorCode.JOURNEY_DOCUMENT_ALREADY_ADDED,
|
throw DomainException.conflict(ErrorCode.JOURNEY_DOCUMENT_ALREADY_ADDED,
|
||||||
"Document already in journey: " + dto.getDocumentId());
|
"Document already in journey: " + dto.getDocumentId());
|
||||||
}
|
}
|
||||||
@@ -249,6 +255,12 @@ public class JourneyItemService {
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isDuplicateDocumentViolation(DataIntegrityViolationException e) {
|
||||||
|
Throwable cause = e.getMostSpecificCause();
|
||||||
|
String message = cause != null ? cause.getMessage() : e.getMessage();
|
||||||
|
return message != null && message.contains("uq_journey_items_geschichte_document");
|
||||||
|
}
|
||||||
|
|
||||||
private static String normalizeNote(String raw) {
|
private static String normalizeNote(String raw) {
|
||||||
if (raw == null || raw.isBlank()) return null;
|
if (raw == null || raw.isBlank()) return null;
|
||||||
return raw.trim();
|
return raw.trim();
|
||||||
|
|||||||
@@ -360,6 +360,27 @@ class JourneyItemServiceTest {
|
|||||||
.isEqualTo(ErrorCode.JOURNEY_DOCUMENT_ALREADY_ADDED));
|
.isEqualTo(ErrorCode.JOURNEY_DOCUMENT_ALREADY_ADDED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void append_rethrows_unrelated_integrity_violations_instead_of_mislabeling_them() {
|
||||||
|
// An FK violation (document deleted between load and flush) must NOT be
|
||||||
|
// translated into "already added" — only the dedup index earns that 409.
|
||||||
|
Geschichte journey = journey(geschichteId);
|
||||||
|
when(geschichteQueryService.findById(geschichteId)).thenReturn(Optional.of(journey));
|
||||||
|
when(journeyItemRepository.countByGeschichteId(geschichteId)).thenReturn(1L);
|
||||||
|
when(journeyItemRepository.existsByGeschichteIdAndDocumentId(eq(geschichteId), any())).thenReturn(false);
|
||||||
|
when(documentService.findSummaryByIdInternal(any())).thenReturn(makeDoc(UUID.randomUUID(), null, List.of(), null, null));
|
||||||
|
when(journeyItemRepository.findMaxPositionByGeschichteId(geschichteId)).thenReturn(Optional.of(10));
|
||||||
|
when(journeyItemRepository.saveAndFlush(any()))
|
||||||
|
.thenThrow(new org.springframework.dao.DataIntegrityViolationException(
|
||||||
|
"insert or update on table \"journey_items\" violates foreign key constraint \"fk_journey_items_document\""));
|
||||||
|
|
||||||
|
JourneyItemCreateDTO dto = new JourneyItemCreateDTO();
|
||||||
|
dto.setDocumentId(UUID.randomUUID());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> journeyItemService.append(geschichteId, dto))
|
||||||
|
.isInstanceOf(org.springframework.dao.DataIntegrityViolationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void append_audits_JOURNEY_ITEM_ADDED() {
|
void append_audits_JOURNEY_ITEM_ADDED() {
|
||||||
Geschichte journey = journey(geschichteId);
|
Geschichte journey = journey(geschichteId);
|
||||||
|
|||||||
Reference in New Issue
Block a user