Keep finalization violations domain-shaped (#53)

This commit is contained in:
nessar committed 2026-08-17 14:18:16 +02:00
1 parent ce53bab698
commit 603b508aec
6 files changed
+144 -70

No files matched your search

@@ -53,7 +53,6 @@ record GuaranteeDeedRequest(
String attestationPath() {
return "guaranteeRequirements[" + requirementIndex + "].attestation";
}
}
record DeedResponse(
@@ -146,27 +146,43 @@ private static PdfUpload upload(MultipartFile file) {
private static FinalizationValidationFailure deedValidationFailure(
LeaseFinalizationApi.GuaranteeDeedRequest request,
LeaseFinalizationValidationFailure failure) {
if (LeaseFinalizationValidationFailure.ATTESTATION.equals(failure.field())) {
return new FinalizationValidationFailure(
List.of(
new LeaseFinalizationApi.FieldValidationError(
request.attestationPath(), failure.code())));
}
if (LeaseFinalizationValidationFailure.FILE.equals(failure.field())) {
return new FinalizationValidationFailure(
List.of(new LeaseFinalizationApi.FieldValidationError(request.filePath(), failure.code())));
}
throw failure;
return switch (failure.rule()) {
case ATTESTATION_REQUIRED -> validationFailure(request.attestationPath(), "required");
case PDF_REQUIRED -> validationFailure(request.filePath(), "required");
case PDF_INVALID -> validationFailure(request.filePath(), "invalid");
default -> throw failure;
};
}
private static FinalizationValidationFailure finalizationValidationFailure(
LeaseFinalizationValidationFailure failure) {
String field =
LeaseFinalizationValidationFailure.FILE.equals(failure.field())
? "signedLease"
: failure.field();
return switch (failure.rule()) {
case ATTESTATION_REQUIRED -> validationFailure("attestation", "required");
case PDF_REQUIRED -> validationFailure("signedLease", "required");
case PDF_INVALID -> validationFailure("signedLease", "invalid");
case ALL_PARTIES_MUST_SIGN -> validationFailure("allPartiesSigned", "required");
case REVISION_MUST_MATCH -> validationFailure("revisionMatches", "required");
case SIGNATURE_DATE_REQUIRED -> validationFailure("lastSignatureDate", "required");
case GUARANTEE_DEEDS_REQUIRED -> validationFailure("guaranteeRequirements", "required");
case WARNING_CONFIRMATION_INVALID -> validationFailure("confirmedWarningCodes", "invalid");
case WARNING_CONFIRMATION_REQUIRED -> validationFailure("confirmedWarningCodes", "required");
case INVENTORY_CORRECTIONS_INCONSISTENT ->
validationFailure("entryInventoryDiscrepancies", "invalid");
case INVENTORY_CORRECTIONS_REQUIRED -> validationFailure("inventoryCorrections", "required");
case INVENTORY_CORRECTIONS_MUST_BE_INITIALED ->
validationFailure("allInventoryCorrectionsInitialed", "required");
case UNKNOWN_CORRECTION_PRIVATE_AREA ->
validationFailure(
"inventoryCorrections[" + failure.correctionIndex() + "].privateAreaId", "invalid");
case UNKNOWN_CORRECTION_INVENTORY_LINE ->
validationFailure(
"inventoryCorrections[" + failure.correctionIndex() + "].inventoryLineId", "invalid");
};
}
private static FinalizationValidationFailure validationFailure(String field, String code) {
return new FinalizationValidationFailure(
List.of(new LeaseFinalizationApi.FieldValidationError(field, failure.code())));
List.of(new LeaseFinalizationApi.FieldValidationError(field, code)));
}
private static final class FinalizationValidationFailure extends RuntimeException {
@@ -120,7 +120,7 @@ public LeaseGuaranteeDeed uploadGuaranteeDeed(
requireAuthor(authorId);
if (!attestation) {
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.ATTESTATION, "required");
LeaseFinalizationValidationFailure.Rule.ATTESTATION_REQUIRED);
}
requirePair(lease, guarantorId, tenantId);
byte[] content = pdf(upload);
@@ -242,13 +242,16 @@ public Result finalizeLease(
}
requireDraftForFinalization(lease);
if (!request.allPartiesSigned()) {
throw new LeaseFinalizationValidationFailure("allPartiesSigned", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.ALL_PARTIES_MUST_SIGN);
}
if (!request.revisionMatches()) {
throw new LeaseFinalizationValidationFailure("revisionMatches", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.REVISION_MUST_MATCH);
}
if (request.lastSignatureDate() == null) {
throw new LeaseFinalizationValidationFailure("lastSignatureDate", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.SIGNATURE_DATE_REQUIRED);
}
requireInventoryCorrectionAttestation(request);
byte[] content = pdf(signedLease);
@@ -258,7 +261,8 @@ public Result finalizeLease(
List<GuaranteeRequirement> requirements =
requirements(landlordId, lease, guaranteeDeeds.findAll(landlordId, leaseId));
if (requirements.stream().anyMatch(requirement -> requirement.deed() == null)) {
throw new LeaseFinalizationValidationFailure("guaranteeRequirements", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.GUARANTEE_DEEDS_REQUIRED);
}
List<LeaseFinalization.LegalOverride> overrides =
overrides(landlordId, lease, revision, request.confirmedWarningCodes(), authorId);
@@ -373,14 +377,16 @@ private List<LeaseFinalization.LegalOverride> overrides(
List<LeaseDocumentWarning> warnings = warnings(landlordId, lease, revision);
if (confirmed.stream()
.anyMatch(code -> warnings.stream().noneMatch(warning -> warning.code().equals(code)))) {
throw new LeaseFinalizationValidationFailure("confirmedWarningCodes", "invalid");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.WARNING_CONFIRMATION_INVALID);
}
Instant now = Instant.now(clock);
return warnings.stream()
.map(
warning -> {
if (!confirmed.contains(warning.code())) {
throw new LeaseFinalizationValidationFailure("confirmedWarningCodes", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.WARNING_CONFIRMATION_REQUIRED);
}
return new LeaseFinalization.LegalOverride(
warning.code(), warning.ruleVersion(), authorId, now);
@@ -443,12 +449,12 @@ private static void validateInventoryCorrections(
LeaseFinalization.InventoryCorrection correction = corrections.get(index);
if (!privateAreaIds.contains(correction.privateAreaId())) {
throw new LeaseFinalizationValidationFailure(
"inventoryCorrections[" + index + "].privateAreaId", "invalid");
LeaseFinalizationValidationFailure.Rule.UNKNOWN_CORRECTION_PRIVATE_AREA, index);
}
if (correction.inventoryLineId() != null
&& !inventoryLineIds.contains(correction.inventoryLineId())) {
throw new LeaseFinalizationValidationFailure(
"inventoryCorrections[" + index + "].inventoryLineId", "invalid");
LeaseFinalizationValidationFailure.Rule.UNKNOWN_CORRECTION_INVENTORY_LINE, index);
}
}
}
@@ -519,12 +525,12 @@ private Lease lease(LandlordId landlordId, LeaseId leaseId) {
private byte[] pdf(PdfUpload upload) {
if (upload == null) {
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.FILE, "required");
LeaseFinalizationValidationFailure.Rule.PDF_REQUIRED);
}
byte[] content = upload.content();
if (content.length == 0) {
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.FILE, "required");
LeaseFinalizationValidationFailure.Rule.PDF_REQUIRED);
}
if (content.length > maximumPdfSize) {
throw new AttachmentTooLarge(maximumPdfSize);
@@ -533,7 +539,7 @@ private byte[] pdf(PdfUpload upload) {
|| content.length < 5
|| !Arrays.equals(Arrays.copyOf(content, 5), "%PDF-".getBytes(StandardCharsets.US_ASCII))) {
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.FILE, "invalid");
LeaseFinalizationValidationFailure.Rule.PDF_INVALID);
}
return content;
}
@@ -557,13 +563,16 @@ private static void requireInventoryCorrectionAttestation(FinalizationRequest re
if (!request.entryInventoryDiscrepancies()
&& (!request.inventoryCorrections().isEmpty()
|| request.allInventoryCorrectionsInitialed())) {
throw new LeaseFinalizationValidationFailure("entryInventoryDiscrepancies", "invalid");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.INVENTORY_CORRECTIONS_INCONSISTENT);
}
if (request.entryInventoryDiscrepancies() && request.inventoryCorrections().isEmpty()) {
throw new LeaseFinalizationValidationFailure("inventoryCorrections", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.INVENTORY_CORRECTIONS_REQUIRED);
}
if (request.entryInventoryDiscrepancies() && !request.allInventoryCorrectionsInitialed()) {
throw new LeaseFinalizationValidationFailure("allInventoryCorrectionsInitialed", "required");
throw new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.INVENTORY_CORRECTIONS_MUST_BE_INITIALED);
}
}
@@ -1,22 +1,44 @@
package fr.nessar.lmnporganizer.rental.application.lease;
public final class LeaseFinalizationValidationFailure extends RuntimeException {
public static final String FILE = "file";
public static final String ATTESTATION = "attestation";
private final String field;
private final String code;
public LeaseFinalizationValidationFailure(String field, String code) {
this.field = field;
this.code = code;
public enum Rule {
ATTESTATION_REQUIRED,
PDF_REQUIRED,
PDF_INVALID,
ALL_PARTIES_MUST_SIGN,
REVISION_MUST_MATCH,
SIGNATURE_DATE_REQUIRED,
GUARANTEE_DEEDS_REQUIRED,
WARNING_CONFIRMATION_INVALID,
WARNING_CONFIRMATION_REQUIRED,
INVENTORY_CORRECTIONS_INCONSISTENT,
INVENTORY_CORRECTIONS_REQUIRED,
INVENTORY_CORRECTIONS_MUST_BE_INITIALED,
UNKNOWN_CORRECTION_PRIVATE_AREA,
UNKNOWN_CORRECTION_INVENTORY_LINE
}
public String field() {
return field;
private final Rule rule;
private final Integer correctionIndex;
public LeaseFinalizationValidationFailure(Rule rule) {
this(rule, null);
}
public String code() {
return code;
public LeaseFinalizationValidationFailure(Rule rule, int correctionIndex) {
this(rule, Integer.valueOf(correctionIndex));
}
private LeaseFinalizationValidationFailure(Rule rule, Integer correctionIndex) {
this.rule = rule;
this.correctionIndex = correctionIndex;
}
public Rule rule() {
return rule;
}
public int correctionIndex() {
return correctionIndex;
}
}
@@ -235,10 +235,29 @@ void guaranteeAndFinalizationValidationUseVisibleStableFieldPaths() throws Excep
.andExpect(jsonPath("$.errors[1].field").value("guaranteeRequirements[0].tenantId"))
.andExpect(jsonPath("$.errors[2].field").value("guaranteeRequirements[0].attestation"));
when(finalizations.uploadGuaranteeDeed(
any(), any(), any(), any(), any(Boolean.class), any(), any()))
.thenThrow(
new LeaseFinalizationValidationFailure(
LeaseFinalizationValidationFailure.Rule.PDF_INVALID));
mockMvc
.perform(
multipart("/api/leases/lease-1/guarantee-deeds")
.file(pdf("file"))
.param("requirementIndex", "0")
.param("guarantorId", "guarantor-1")
.param("tenantId", "tenant-1")
.param("attestation", "true")
.with(landlord())
.with(csrf()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[0].field").value("guaranteeRequirements[0].file"))
.andExpect(jsonPath("$.errors[0].code").value("invalid"));
when(finalizations.finalizeLease(any(), any(), any(), any(), any()))
.thenThrow(
new LeaseFinalizationValidationFailure(
"inventoryCorrections[0].privateAreaId", "invalid"));
LeaseFinalizationValidationFailure.Rule.UNKNOWN_CORRECTION_PRIVATE_AREA, 0));
mockMvc
.perform(
multipart("/api/leases/lease-1/finalization")
@@ -248,8 +267,7 @@ void guaranteeAndFinalizationValidationUseVisibleStableFieldPaths() throws Excep
.with(csrf()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("validation_failed"))
.andExpect(
jsonPath("$.errors[0].field").value("inventoryCorrections[0].privateAreaId"))
.andExpect(jsonPath("$.errors[0].field").value("inventoryCorrections[0].privateAreaId"))
.andExpect(jsonPath("$.errors[0].code").value("invalid"));
}
@@ -503,28 +503,38 @@ void
successfulFinalizationSetup(draft, revision);
LeaseFinalizationService service = service(clock("2026-07-19T12:00:00Z"));
assertThrows(
LeaseFinalizationValidationFailure.class,
() ->
service.finalizeLease(
LANDLORD_ID,
LEASE_ID,
correctionRequest(
new LeaseFinalization.InventoryCorrection(
"correction-1", "line-1", "other-area", "Chaise", 0, "Absente")),
upload(PDF),
"subject-1"));
assertThrows(
LeaseFinalizationValidationFailure.class,
() ->
service.finalizeLease(
LANDLORD_ID,
LEASE_ID,
correctionRequest(
new LeaseFinalization.InventoryCorrection(
"correction-1", "other-line", "area-1", "Chaise", 0, "Absente")),
upload(PDF),
"subject-1"));
LeaseFinalizationValidationFailure privateAreaFailure =
assertThrows(
LeaseFinalizationValidationFailure.class,
() ->
service.finalizeLease(
LANDLORD_ID,
LEASE_ID,
correctionRequest(
new LeaseFinalization.InventoryCorrection(
"correction-1", "line-1", "other-area", "Chaise", 0, "Absente")),
upload(PDF),
"subject-1"));
assertEquals(
LeaseFinalizationValidationFailure.Rule.UNKNOWN_CORRECTION_PRIVATE_AREA,
privateAreaFailure.rule());
assertEquals(0, privateAreaFailure.correctionIndex());
LeaseFinalizationValidationFailure inventoryLineFailure =
assertThrows(
LeaseFinalizationValidationFailure.class,
() ->
service.finalizeLease(
LANDLORD_ID,
LEASE_ID,
correctionRequest(
new LeaseFinalization.InventoryCorrection(
"correction-1", "other-line", "area-1", "Chaise", 0, "Absente")),
upload(PDF),
"subject-1"));
assertEquals(
LeaseFinalizationValidationFailure.Rule.UNKNOWN_CORRECTION_INVENTORY_LINE,
inventoryLineFailure.rule());
assertEquals(0, inventoryLineFailure.correctionIndex());
LeaseFinalizationService.Result result =
service.finalizeLease(