feat(person): store birth/death as LocalDate + DatePrecision
Entity swap mirroring Document.metaDatePrecision; PersonUpdateDTO takes date + precision; validateLifeDates (badRequest BIRTH_AFTER_DEATH / INVALID_DATE_PRECISION) replaces validateYears; preferHumanDate keeps DAY/MONTH/SEASON hand-entered dates on re-import and refreshes YEAR/UNKNOWN from the canonical year (ADR-025 extension); PersonUpsertCommand stays year-shaped. Native queries project EXTRACT(YEAR ...) so PersonSummaryDTO and PersonNodeDTO stay year-shaped, null-safe for undated persons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
import org.raddatz.familienarchiv.document.Document;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
import org.raddatz.familienarchiv.person.PersonNameAlias;
|
||||
@@ -22,6 +23,7 @@ import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -572,18 +574,53 @@ class PersonControllerTest {
|
||||
void createPerson_returns200_withAllSixFields() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person saved = Person.builder().id(id).firstName("Maria").lastName("Raddatz")
|
||||
.alias("Oma Maria").birthYear(1901).deathYear(1975).notes("Some notes").build();
|
||||
.alias("Oma Maria")
|
||||
.birthDate(LocalDate.of(1901, 3, 14)).birthDatePrecision(DatePrecision.DAY)
|
||||
.deathDate(LocalDate.of(1975, 1, 1)).deathDatePrecision(DatePrecision.YEAR)
|
||||
.notes("Some notes").build();
|
||||
when(personService.createPerson(any(org.raddatz.familienarchiv.person.PersonUpdateDTO.class))).thenReturn(saved);
|
||||
|
||||
mockMvc.perform(post("/api/persons").with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Maria\",\"lastName\":\"Raddatz\"," +
|
||||
"\"alias\":\"Oma Maria\",\"birthYear\":1901,\"deathYear\":1975," +
|
||||
"\"alias\":\"Oma Maria\"," +
|
||||
"\"birthDate\":\"1901-03-14\",\"birthDatePrecision\":\"DAY\"," +
|
||||
"\"deathDate\":\"1975-01-01\",\"deathDatePrecision\":\"YEAR\"," +
|
||||
"\"notes\":\"Some notes\",\"personType\":\"PERSON\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("Maria"))
|
||||
.andExpect(jsonPath("$.alias").value("Oma Maria"))
|
||||
.andExpect(jsonPath("$.birthYear").value(1901));
|
||||
.andExpect(jsonPath("$.birthDate").value("1901-03-14"))
|
||||
.andExpect(jsonPath("$.birthDatePrecision").value("DAY"));
|
||||
}
|
||||
|
||||
// ─── #773: malformed date payloads return structured 400s, not Jackson traces ──
|
||||
// Jackson rejects unknown enum values by default. Verified 2026-06-12: the only
|
||||
// DeserializationFeature hit in src/main is RestClientOcrClient's private ObjectMapper
|
||||
// (OCR HTTP client) — the Spring MVC mapper has no READ_UNKNOWN_ENUM_VALUES_* override.
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "WRITE_ALL")
|
||||
void updatePerson_returns400WithStructuredErrorCode_whenPrecisionEnumInvalid() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
mockMvc.perform(put("/api/persons/{id}", id).with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"," +
|
||||
"\"birthDate\":\"1901-03-14\",\"birthDatePrecision\":\"INVALID_VALUE\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "WRITE_ALL")
|
||||
void updatePerson_returns400WithStructuredErrorCode_whenBirthDateNotADate() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
mockMvc.perform(put("/api/persons/{id}", id).with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\",\"personType\":\"PERSON\"," +
|
||||
"\"birthDate\":\"not-a-date\",\"birthDatePrecision\":\"DAY\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
// ─── Phase 1.2: @Size constraints ─────────────────────────────────────────
|
||||
|
||||
@@ -5,7 +5,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -97,24 +99,120 @@ class PersonImportUpsertTest {
|
||||
assertThat(result.getNotes()).isEqualTo("Nichte von Herbert");
|
||||
}
|
||||
|
||||
// ─── life dates (ADR-025 extension via preferHumanDate, #773) ─────────────
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_fillsBlankYears_butPreservesHumanEditedYears_onReimport() {
|
||||
// Existing has a human-set birthYear and a blank deathYear.
|
||||
Person existing = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram")
|
||||
.lastName("Cram").birthYear(1890).deathYear(null).build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
|
||||
void upsertBySourceRef_preservesDayPrecisionDate_onReimportWithDifferentYear() {
|
||||
// A human entered the exact birthday in-app; the spreadsheet only knows a year.
|
||||
Person handDated = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
|
||||
.birthDate(LocalDate.of(1890, 3, 14)).birthDatePrecision(DatePrecision.DAY).build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(handDated));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.birthYear(1888).deathYear(1965)
|
||||
.birthYear(1888)
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getBirthYear()).isEqualTo(1890); // human value kept
|
||||
assertThat(result.getDeathYear()).isEqualTo(1965); // blank filled from canonical
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 3, 14));
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_preservesMonthPrecisionDate_onReimport() {
|
||||
Person handDated = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
|
||||
.deathDate(LocalDate.of(1944, 11, 1)).deathDatePrecision(DatePrecision.MONTH).build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(handDated));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.deathYear(1945)
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1944, 11, 1));
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.MONTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_refreshesYearPrecisionDate_whenSpreadsheetYearChanges() {
|
||||
// YEAR precision means "the importer's year" — a corrected spreadsheet year wins.
|
||||
Person yearOnly = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram")
|
||||
.birthDate(LocalDate.of(1890, 1, 1)).birthDatePrecision(DatePrecision.YEAR).build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(yearOnly));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.birthYear(1888)
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1888, 1, 1));
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_fillsEmptyDateAtYearPrecision_onReimport() {
|
||||
Person noDates = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram").build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(noDates));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.deathYear(1965)
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1965, 1, 1));
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_keepsDatesEmpty_whenSpreadsheetHasNoYear() {
|
||||
Person noDates = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram").lastName("Cram").build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(noDates));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getBirthDate()).isNull();
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
|
||||
assertThat(result.getDeathDate()).isNull();
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_translatesYearToDate_onFirstImport() {
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.empty());
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").lastName("Cram")
|
||||
.birthYear(1890).deathYear(1965)
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 1, 1));
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1965, 1, 1));
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,6 +18,9 @@ import org.raddatz.familienarchiv.document.DocumentRepository;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -910,4 +913,61 @@ class PersonRepositoryTest {
|
||||
.setParameter(1, blockId).getSingleResult();
|
||||
assertThat(text).isEqualTo("Brief an @Auguste Raddatz und @Clara Cram");
|
||||
}
|
||||
|
||||
// ─── #773: PersonSummaryDTO year projection from birth_date/death_date ──────
|
||||
|
||||
@Test
|
||||
void findAllWithDocumentCount_derivesYearsFromDates_nullSafe() {
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Maria").lastName("Datiert")
|
||||
.birthDate(LocalDate.of(1901, 3, 14)).birthDatePrecision(DatePrecision.DAY)
|
||||
.build());
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Nora").lastName("Undatiert")
|
||||
.build());
|
||||
entityManager.flush();
|
||||
|
||||
List<PersonSummaryDTO> all = personRepository.findAllWithDocumentCount();
|
||||
|
||||
PersonSummaryDTO dated = all.stream()
|
||||
.filter(p -> "Datiert".equals(p.getLastName())).findFirst().orElseThrow();
|
||||
assertThat(dated.getBirthYear()).isEqualTo(1901);
|
||||
assertThat(dated.getDeathYear()).isNull();
|
||||
PersonSummaryDTO undated = all.stream()
|
||||
.filter(p -> "Undatiert".equals(p.getLastName())).findFirst().orElseThrow();
|
||||
assertThat(undated.getBirthYear()).isNull();
|
||||
assertThat(undated.getDeathYear()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchWithDocumentCount_groupByPath_derivesYearsFromDates() {
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Herbert").lastName("Gruppiert")
|
||||
.birthDate(LocalDate.of(1899, 1, 1)).birthDatePrecision(DatePrecision.YEAR)
|
||||
.deathDate(LocalDate.of(1972, 6, 12)).deathDatePrecision(DatePrecision.DAY)
|
||||
.build());
|
||||
entityManager.flush();
|
||||
|
||||
List<PersonSummaryDTO> found = personRepository.searchWithDocumentCount("Gruppiert");
|
||||
|
||||
assertThat(found).hasSize(1);
|
||||
assertThat(found.get(0).getBirthYear()).isEqualTo(1899);
|
||||
assertThat(found.get(0).getDeathYear()).isEqualTo(1972);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByFilter_derivesYearsFromDates() {
|
||||
personRepository.save(Person.builder()
|
||||
.firstName("Filtriert").lastName("Person")
|
||||
.birthDate(LocalDate.of(1920, 1, 1)).birthDatePrecision(DatePrecision.YEAR)
|
||||
.build());
|
||||
entityManager.flush();
|
||||
|
||||
List<PersonSummaryDTO> found = personRepository.findByFilter(
|
||||
null, null, null, null, false, "Filtriert", 10, 0);
|
||||
|
||||
assertThat(found).hasSize(1);
|
||||
assertThat(found.get(0).getBirthYear()).isEqualTo(1920);
|
||||
assertThat(found.get(0).getDeathYear()).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.raddatz.familienarchiv.person.PersonNameAliasDTO;
|
||||
import org.raddatz.familienarchiv.person.PersonSummaryDTO;
|
||||
import org.raddatz.familienarchiv.person.PersonUpdateDTO;
|
||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
import org.raddatz.familienarchiv.person.PersonNameAlias;
|
||||
import org.raddatz.familienarchiv.person.PersonNameAliasType;
|
||||
@@ -17,6 +19,7 @@ import org.raddatz.familienarchiv.person.PersonNameAliasRepository;
|
||||
import org.raddatz.familienarchiv.person.PersonRepository;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -241,27 +244,49 @@ class PersonServiceTest {
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Maria"); dto.setLastName("Raddatz"); dto.setAlias("Oma Maria");
|
||||
dto.setBirthYear(1901); dto.setDeathYear(1975); dto.setNotes("Some notes");
|
||||
dto.setBirthDate(LocalDate.of(1901, 3, 14)); dto.setBirthDatePrecision(DatePrecision.DAY);
|
||||
dto.setDeathDate(LocalDate.of(1975, 11, 2)); dto.setDeathDatePrecision(DatePrecision.DAY);
|
||||
dto.setNotes("Some notes");
|
||||
|
||||
Person result = personService.createPerson(dto);
|
||||
|
||||
assertThat(result.getFirstName()).isEqualTo("Maria");
|
||||
assertThat(result.getLastName()).isEqualTo("Raddatz");
|
||||
assertThat(result.getAlias()).isEqualTo("Oma Maria");
|
||||
assertThat(result.getBirthYear()).isEqualTo(1901);
|
||||
assertThat(result.getDeathYear()).isEqualTo(1975);
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1901, 3, 14));
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1975, 11, 2));
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
|
||||
assertThat(result.getNotes()).isEqualTo("Some notes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPerson_dto_yearValidationFires_whenBirthYearNegative() {
|
||||
void createPerson_dto_rejectsDateWithUnknownPrecision() {
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Test"); dto.setBirthYear(-1);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Test");
|
||||
dto.setBirthDate(LocalDate.of(1901, 3, 14)); dto.setBirthDatePrecision(DatePrecision.UNKNOWN);
|
||||
|
||||
assertThatThrownBy(() -> personService.createPerson(dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
.isInstanceOf(DomainException.class)
|
||||
.satisfies(e -> {
|
||||
assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
|
||||
assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPerson_dto_treatsNullPrecisionWithNullDateAsUnknown() {
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Test"); dto.setPersonType(PersonType.PERSON);
|
||||
|
||||
Person result = personService.createPerson(dto);
|
||||
|
||||
assertThat(result.getBirthDate()).isNull();
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
|
||||
assertThat(result.getDeathDate()).isNull();
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -600,114 +625,135 @@ class PersonServiceTest {
|
||||
assertThat(result.getNotes()).isNull();
|
||||
}
|
||||
|
||||
// ─── updatePerson (birth/death years) ────────────────────────────────────
|
||||
// ─── updatePerson (birth/death dates) ────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void updatePerson_persistsBirthAndDeathYear() {
|
||||
void updatePerson_persistsBirthAndDeathDateWithPrecision() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
|
||||
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(1890); dto.setDeathYear(1965);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1890, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
|
||||
dto.setDeathDate(LocalDate.of(1965, 6, 12)); dto.setDeathDatePrecision(DatePrecision.DAY);
|
||||
Person result = personService.updatePerson(id, dto);
|
||||
|
||||
assertThat(result.getBirthYear()).isEqualTo(1890);
|
||||
assertThat(result.getDeathYear()).isEqualTo(1965);
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 1, 1));
|
||||
assertThat(result.getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1965, 6, 12));
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_throwsBadRequest_whenBirthYearAfterDeathYear() {
|
||||
void updatePerson_throwsBirthAfterDeath_whenBirthDateAfterDeathDate() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(1970); dto.setDeathYear(1950);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1970, 5, 1)); dto.setBirthDatePrecision(DatePrecision.DAY);
|
||||
dto.setDeathDate(LocalDate.of(1950, 5, 1)); dto.setDeathDatePrecision(DatePrecision.DAY);
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
.isInstanceOf(DomainException.class)
|
||||
.satisfies(e -> {
|
||||
assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.BIRTH_AFTER_DEATH);
|
||||
assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_doesNotThrow_whenBirthYearNonNullButDeathYearIsNull() {
|
||||
// Covers A && B short-circuit: birthYear != null (true) but deathYear == null (false) → no throw
|
||||
void updatePerson_throwsBirthAfterDeath_onMixedPrecisionLateBirthday() {
|
||||
// Known limitation (#773): DAY-precision birth late in the death's YEAR-precision year
|
||||
// compares against the year's backfilled Jan 1st and is rejected. The error message
|
||||
// carries the workaround hint via the BIRTH_AFTER_DEATH i18n key.
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1901, 11, 15)); dto.setBirthDatePrecision(DatePrecision.DAY);
|
||||
dto.setDeathDate(LocalDate.of(1901, 1, 1)); dto.setDeathDatePrecision(DatePrecision.YEAR);
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(DomainException.class)
|
||||
.extracting(e -> ((DomainException) e).getCode())
|
||||
.isEqualTo(ErrorCode.BIRTH_AFTER_DEATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_doesNotThrow_whenBirthDateSetButDeathDateNull() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
|
||||
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(1890); dto.setDeathYear(null);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1890, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
|
||||
Person result = personService.updatePerson(id, dto);
|
||||
|
||||
assertThat(result.getBirthYear()).isEqualTo(1890);
|
||||
assertThat(result.getDeathYear()).isNull();
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1890, 1, 1));
|
||||
assertThat(result.getDeathDate()).isNull();
|
||||
assertThat(result.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_allowsSameYear() {
|
||||
void updatePerson_allowsEqualBirthAndDeathDate() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
|
||||
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(1900); dto.setDeathYear(1900);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1900, 1, 1)); dto.setBirthDatePrecision(DatePrecision.YEAR);
|
||||
dto.setDeathDate(LocalDate.of(1900, 1, 1)); dto.setDeathDatePrecision(DatePrecision.YEAR);
|
||||
Person result = personService.updatePerson(id, dto);
|
||||
|
||||
assertThat(result.getBirthYear()).isEqualTo(1900);
|
||||
assertThat(result.getDeathYear()).isEqualTo(1900);
|
||||
assertThat(result.getBirthDate()).isEqualTo(LocalDate.of(1900, 1, 1));
|
||||
assertThat(result.getDeathDate()).isEqualTo(LocalDate.of(1900, 1, 1));
|
||||
}
|
||||
|
||||
// ─── Phase 1.3: Year range bounds (> 0) ──────────────────────────────────
|
||||
// ─── Date/precision coherence (V76 CHECK constraint mirror) ─────────────
|
||||
|
||||
@Test
|
||||
void updatePerson_throwsBadRequest_whenBirthYearIsZero() {
|
||||
void updatePerson_throwsInvalidDatePrecision_whenDatePresentButPrecisionUnknown() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(0);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setDeathDate(LocalDate.of(1944, 11, 2)); dto.setDeathDatePrecision(DatePrecision.UNKNOWN);
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
.isInstanceOf(DomainException.class)
|
||||
.satisfies(e -> {
|
||||
assertThat(((DomainException) e).getCode()).isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
|
||||
assertThat(((DomainException) e).getStatus().value()).isEqualTo(400);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_throwsBadRequest_whenBirthYearIsNegative() {
|
||||
void updatePerson_throwsInvalidDatePrecision_whenDatePresentButPrecisionNull() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setBirthYear(-5);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDate(LocalDate.of(1901, 3, 14));
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
.isInstanceOf(DomainException.class)
|
||||
.extracting(e -> ((DomainException) e).getCode())
|
||||
.isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_throwsBadRequest_whenDeathYearIsZero() {
|
||||
void updatePerson_throwsInvalidDatePrecision_whenPrecisionSetWithoutDate() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setDeathYear(0);
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt");
|
||||
dto.setBirthDatePrecision(DatePrecision.DAY);
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatePerson_throwsBadRequest_whenDeathYearIsNegative() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||
dto.setFirstName("Anna"); dto.setLastName("Alt"); dto.setDeathYear(-10);
|
||||
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||
.isEqualTo(400);
|
||||
.isInstanceOf(DomainException.class)
|
||||
.extracting(e -> ((DomainException) e).getCode())
|
||||
.isEqualTo(ErrorCode.INVALID_DATE_PRECISION);
|
||||
}
|
||||
|
||||
// ─── findCorrespondents ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user