refactor(document): share skip-null date-field resolution between save and projection (#726)
Extract effectivePrecision/effectiveMetaDateEnd/effectiveMetaDateRaw, used by both applyDatePrecision (the real setters) and projectedState (the title projection), so the two can no longer drift — addresses review feedback (Markus/Felix/Sara). Writing a stored value back when the DTO omits a field is a harmless no-op, so behaviour is unchanged (185 existing DocumentServiceTest cases stay green). Also documents the file-replace "treat as manual" path inline at the reassignment site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -431,7 +431,11 @@ public class DocumentService {
|
|||||||
doc.setScriptType(dto.getScriptType());
|
doc.setScriptType(dto.getScriptType());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Datei austauschen (nur wenn eine neue ausgewählt wurde)
|
// 4. Datei austauschen (nur wenn eine neue ausgewählt wurde).
|
||||||
|
// NB (#726): this reassigns originalFilename to the uploaded file's name. The title's index
|
||||||
|
// segment is originalFilename, so after a replace the stored title no longer matches
|
||||||
|
// build(currentState) and the row is treated as manual — neither save-time nor backfill
|
||||||
|
// rewrites it. Accepted fail-safe (ADR-031), and autoTitleBefore was already captured above.
|
||||||
boolean fileReplaced = newFile != null && !newFile.isEmpty();
|
boolean fileReplaced = newFile != null && !newFile.isEmpty();
|
||||||
if (fileReplaced) {
|
if (fileReplaced) {
|
||||||
FileService.UploadResult upload = fileService.uploadFile(newFile, newFile.getOriginalFilename());
|
FileService.UploadResult upload = fileService.uploadFile(newFile, newFile.getOriginalFilename());
|
||||||
@@ -479,44 +483,49 @@ public class DocumentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The document state the regenerated title is built from, mirroring {@code updateDocument}'s
|
* The document state the regenerated title is built from. It is composed from the SAME
|
||||||
* setter asymmetry exactly: {@code documentDate}/{@code location} are overwritten from the DTO
|
* resolvers the real setters use — {@code documentDate}/{@code location} overwritten from the
|
||||||
* (a null value clears the field), while precision/end/raw are taken from the DTO only when
|
* DTO (a null value clears the field), precision/end/raw resolved skip-null via
|
||||||
* non-null and otherwise kept from the stored entity (see {@link #applyDatePrecision}). The
|
* {@link #effectivePrecision}/{@link #effectiveMetaDateEnd}/{@link #effectiveMetaDateRaw} — so
|
||||||
* index ({@code originalFilename}) is never touched by a metadata edit. A mismatch here would
|
* the projection cannot drift from {@link #updateDocument}. The index ({@code originalFilename})
|
||||||
* silently produce a wrong label, so it is kept lock-step with the real setters.
|
* is never touched by a metadata edit.
|
||||||
*/
|
*/
|
||||||
private Document projectedState(Document doc, DocumentUpdateDTO dto) {
|
private Document projectedState(Document doc, DocumentUpdateDTO dto) {
|
||||||
return Document.builder()
|
return Document.builder()
|
||||||
.originalFilename(doc.getOriginalFilename())
|
.originalFilename(doc.getOriginalFilename())
|
||||||
.documentDate(dto.getDocumentDate())
|
.documentDate(dto.getDocumentDate())
|
||||||
.location(dto.getLocation())
|
.location(dto.getLocation())
|
||||||
.metaDatePrecision(dto.getMetaDatePrecision() != null
|
.metaDatePrecision(effectivePrecision(doc, dto))
|
||||||
? dto.getMetaDatePrecision() : doc.getMetaDatePrecision())
|
.metaDateEnd(effectiveMetaDateEnd(doc, dto))
|
||||||
.metaDateEnd(dto.getMetaDateEnd() != null
|
.metaDateRaw(effectiveMetaDateRaw(doc, dto))
|
||||||
? dto.getMetaDateEnd() : doc.getMetaDateEnd())
|
|
||||||
.metaDateRaw(dto.getMetaDateRaw() != null
|
|
||||||
? dto.getMetaDateRaw() : doc.getMetaDateRaw())
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the three date-precision fields only when the DTO carries them.
|
* Applies the three date-precision fields skip-null: a null DTO field means "not submitted",
|
||||||
* A null field means "not submitted" — overwriting the stored value with null
|
* so the stored value is kept rather than overwritten with null — which would fabricate a
|
||||||
* would fabricate a precision the user never chose, the exact dishonesty #666
|
* precision the user never chose, the exact dishonesty #666 exists to prevent. Expressed via
|
||||||
* exists to prevent. A row with a genuinely-unknown precision must keep it when
|
* the shared {@code effective*} resolvers so {@link #projectedState} stays lock-step (writing
|
||||||
* an unrelated edit (e.g. a location typo) is saved.
|
* the stored value back when the DTO omits a field is a harmless no-op).
|
||||||
*/
|
*/
|
||||||
private void applyDatePrecision(Document doc, DocumentUpdateDTO dto) {
|
private void applyDatePrecision(Document doc, DocumentUpdateDTO dto) {
|
||||||
if (dto.getMetaDatePrecision() != null) {
|
doc.setMetaDatePrecision(effectivePrecision(doc, dto));
|
||||||
doc.setMetaDatePrecision(dto.getMetaDatePrecision());
|
doc.setMetaDateEnd(effectiveMetaDateEnd(doc, dto));
|
||||||
}
|
doc.setMetaDateRaw(effectiveMetaDateRaw(doc, dto));
|
||||||
if (dto.getMetaDateEnd() != null) {
|
}
|
||||||
doc.setMetaDateEnd(dto.getMetaDateEnd());
|
|
||||||
}
|
// Skip-null date-field resolution shared by applyDatePrecision (the real setters) and
|
||||||
if (dto.getMetaDateRaw() != null) {
|
// projectedState (the title projection) — the single rule keeps them from diverging (#726).
|
||||||
doc.setMetaDateRaw(dto.getMetaDateRaw());
|
private static DatePrecision effectivePrecision(Document doc, DocumentUpdateDTO dto) {
|
||||||
}
|
return dto.getMetaDatePrecision() != null ? dto.getMetaDatePrecision() : doc.getMetaDatePrecision();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate effectiveMetaDateEnd(Document doc, DocumentUpdateDTO dto) {
|
||||||
|
return dto.getMetaDateEnd() != null ? dto.getMetaDateEnd() : doc.getMetaDateEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String effectiveMetaDateRaw(Document doc, DocumentUpdateDTO dto) {
|
||||||
|
return dto.getMetaDateRaw() != null ? dto.getMetaDateRaw() : doc.getMetaDateRaw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user