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(