From 603b508aeceb354e02476b6f0050b016e6051953 Mon Sep 17 00:00:00 2001
From: Nessar
Date: Fri, 24 Jul 2026 19:55:16 +0200
Subject: [PATCH] Keep finalization violations domain-shaped (#53)
---
.../in/web/lease/LeaseFinalizationApi.java | 1 -
.../lease/LeaseFinalizationController.java | 48 +++++++++++------
.../lease/LeaseFinalizationService.java | 39 ++++++++------
.../LeaseFinalizationValidationFailure.java | 48 ++++++++++++-----
.../web/lease/LeaseFinalizationWebTest.java | 24 +++++++--
.../lease/LeaseFinalizationServiceTest.java | 54 +++++++++++--------
6 files changed, 144 insertions(+), 70 deletions(-)
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationApi.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationApi.java
index 9021196..96ee028 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationApi.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationApi.java
@@ -53,7 +53,6 @@ record GuaranteeDeedRequest(
String attestationPath() {
return "guaranteeRequirements[" + requirementIndex + "].attestation";
}
-
}
record DeedResponse(
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationController.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationController.java
index ce59b0c..ba9b0dc 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationController.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationController.java
@@ -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 {
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationService.java
index 3388801..9b6f0db 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationService.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationService.java
@@ -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 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 overrides =
overrides(landlordId, lease, revision, request.confirmedWarningCodes(), authorId);
@@ -373,14 +377,16 @@ private List overrides(
List 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);
}
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationValidationFailure.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationValidationFailure.java
index 7834c6d..3850e68 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationValidationFailure.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationValidationFailure.java
@@ -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;
}
}
diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationWebTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationWebTest.java
index df0939b..5fca094 100644
--- a/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationWebTest.java
+++ b/backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/web/lease/LeaseFinalizationWebTest.java
@@ -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"));
}
diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationServiceTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationServiceTest.java
index fceedf6..45ea9d9 100644
--- a/backend/src/test/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationServiceTest.java
+++ b/backend/src/test/java/fr/nessar/lmnporganizer/rental/application/lease/LeaseFinalizationServiceTest.java
@@ -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(