Complete validation parity audit

This commit is contained in:
nessar committed 2026-08-17 14:18:17 +02:00
1 parent c10a4b2fb2
commit 15c30986aa
39 files changed
+1328 -372

No files matched your search

+268 -230
View File
@@ -1,285 +1,323 @@
# Validation Parity Checklist
This checklist acts as implementation evidence for Epic #42 and Issue #59, mapping every user-editable Angular form and mutable HTTP command to its backend request constraints, domain rules, error handling behavior, and user interaction contract.
This document accounts for the application's user-editable Angular forms and mutable rental HTTP
commands for Epic #42 and Issue #59. It records the implemented contracts; it is not a claim that
unrecorded CI or local test gates passed.
---
The backend remains authoritative. Angular mirrors only important save-time constraints that can
give immediate, actionable guidance. The implementation uses the existing Jakarta validation,
domain rules, Angular forms, and `SaveFailure` contract; it does not add a generated schema, shared
validation framework, state-management mechanism, or dependency.
## 1. Landlord Profile & Settings
## Shared failure and interaction contract
- **Angular components**: `LandlordProfileFormComponent`, `LandlordProfileCompletionDialogComponent`, `SettingsPageComponent`
- **HTTP Endpoint**: `PUT /api/landlord/profile`
- **Backend Controller**: `LandlordProfileController`
- **Request DTO / Invariants**:
- `firstName`: `@NotBlank`, max 100
- `lastName`: `@NotBlank`, max 100
- `email`: `@NotBlank`, `@Email`
- `phone`: optional/valid phone number format
- `address`: optional/text
- **Error Mapping**:
- `firstName` / `lastName` `required`: "Le prénom/nom est obligatoire."
- `email` `invalid`: "Format d'adresse e-mail invalide."
- `EmailAlreadyUsed` (409 Conflict): "Cet e-mail est déjà utilisé."
- **Interaction Contract**:
- Touched-or-submit validation timing
- Required field markers (`*`) visible
- First invalid input receives focus on submit failure
- Inline server errors clear independently when edited
- Pending spinner and button disabled state during save lifecycle
- Request validation failures use HTTP 400 and
`{ "code": "validation_failed", "errors": [{ "field": "...", "code": "..." }] }`.
- Conflicts use HTTP 409, oversized uploads use HTTP 413, unavailable IRL data uses HTTP 503,
and unexpected failures use HTTP 500. HTTP 422 is not part of this contract.
- Angular maps known field/code pairs to French inline guidance. Unknown fields/codes, network
failures, conflicts without recognized guidance, and unexpected failures retain a safe visible
operation-level fallback.
- Save forms retain entered values after failure, prevent duplicate submission while their own
operation is pending, and clear a server field error when that field is edited.
- Required markers, touched-or-submit timing, and first-invalid focus are implemented by each form
because the controls and nested paths differ; there is no generated or cross-stack validation
layer.
---
## 1. Landlord profile and settings
## 2. Apartment Core, Legal Profile & Private Areas
- **Angular**: `LandlordProfileForm`, `LandlordProfileCompletionDialog`, `SettingsPage`
- **Mutable command**: `PUT /api/landlord-profile`
- **Backend**: `LandlordProfileController`, `LandlordProfileApi.Request`
- **Authoritative constraints**:
- `firstName`, `lastName`, `email`, `phone`, and `address` are `@NotBlank`.
- `email` is `@Email`.
- `phoneValid` is an `@AssertTrue` validation of the shared `PhoneNumber` domain type.
- **Angular mirror**: the five required controls, email syntax, and phone syntax are checked before
save; known server errors are shown inline and focused. Settings and the completion dialog keep
operation-local loading/saving state.
- **Angular components**: `ApartmentFormComponent`, `ApartmentLegalProfileFormComponent`, `PrivateAreaEditorComponent`, `ApartmentsPageComponent`
- **HTTP Endpoints**:
There is no `/api/landlord/profile` route and no 100-character DTO limit on the name fields.
## 2. Apartments, legal profile, and private areas
- **Angular**: `ApartmentForm`, `ApartmentLegalProfileForm`, `PrivateAreaEditor`, `ApartmentsPage`
- **Mutable commands**:
- `POST /api/apartments`
- `PUT /api/apartments/{id}`
- `PUT /api/apartments/{id}/legal-profile`
- `PUT /api/apartments/{id}/private-areas`
- **Backend Controller**: `ApartmentController`
- **Request DTO / Invariants**:
- `name`: `@NotBlank`
- `address`: `@NotBlank`
- `purchasePrice`: `@NotNull`, `@Min(0)`
- `description`: optional text
- `legalProfile`: `buildingYear` >= 1800, `coproprietyLots` >= 0, valid ratings
- `privateAreas[i].label`: `@NotBlank`
- `privateAreas[i].surfaceArea`: optional `@Min(0)`
- **Error Mapping**:
- `name` / `address` `not_blank`: "Ce champ est obligatoire."
- `purchasePrice` `min`: "Le prix doit être positif."
- `privateAreas[i].label` `not_blank`: "Le libellé de l'espace privatif est obligatoire."
- `ReferencedResource` (409 Conflict): "Cet espace privatif est référencé par un bail actif."
- **Interaction Contract**:
- Section-level error navigation & invalid section expansion
- Indexed field path resolution (`privateAreas[0].label`) mapped directly to nested controls
- Local error state clearing on field edit
- `PUT /api/apartments/{apartmentId}`
- `DELETE /api/apartments/{apartmentId}`
- **Backend**: `ApartmentController`, `ApartmentApi.Request`
- **Authoritative constraints**:
- `name` and `address` are `@NotBlank`.
- `purchasePrice` is `@NotNull @Positive`.
- `privateAreas` is non-null; every item has a non-blank `id` and `label`, non-null `family` and
`subtype`, and a family/subtype compatibility rule.
- Legal-profile surface and annual energy cost are positive or zero when present. Providing an
energy cost requires a reference year between 1900 and 2100. Construction period, energy
rating, and French department code use explicit validity rules.
- **Angular mirror**: required core fields, strictly positive purchase price, legal-profile numeric
bounds and department code, and private-area indexed constraints are checked before save.
Structured paths such as `privateAreas[0].label` focus and clear the matching row.
---
Legal profile and private areas are part of the apartment request. There are no separate
`/legal-profile` or `/private-areas` write endpoints, and the contract has no `buildingYear`,
`coproprietyLots`, or private-area `surfaceArea` field.
## 3. Apartment Documents
## 3. Apartment documents
- **Angular components**: `ApartmentDocumentManagerComponent`
- **HTTP Endpoints**:
- `POST /api/apartments/{id}/documents` (multipart)
- `DELETE /api/apartments/{id}/documents/{documentId}`
- **Backend Controller**: `ApartmentDocumentController`
- **Request DTO / Invariants**:
- `file`: required non-empty PDF/image, <= max upload limit
- `type`: `@NotBlank` (valid `ApartmentDocumentType`)
- `establishedOn`: valid ISO date
- `expiresOn`: valid ISO date >= establishedOn
- **Error Mapping**:
- `file` `required`: "Veuillez sélectionner un fichier."
- `file` `too_large`: "La pièce jointe dépasse la taille maximale autorisée."
- `establishedOn` / `expiresOn` `invalid`: "Format de date invalide."
- **Interaction Contract**:
- Immediate file selection validation
- Progress indicator per upload
- Operation-local error alert with retry option
- **Angular**: `ApartmentDocumentManager`
- **Mutable commands**:
- `POST /api/apartments/{apartmentId}/documents` (multipart)
- `DELETE /api/apartments/{apartmentId}/documents/{documentId}`
- **Backend**: `ApartmentDocumentController`, `ApartmentDocumentApi.UploadRequest`
- **Multipart contract**:
- `file`: required, non-empty PDF whose content is recognized as PDF.
- `type`: required valid `ApartmentDocumentType` code.
- `establishedOn`: required ISO local date.
- `expiresOn`: optional ISO local date, not before `establishedOn`.
- The attachment limit is configurable through `lmnp.attachments.max-size` and defaults to
250 MB; Spring's multipart request limit defaults to 251 MB.
- **Angular mirror**: PDF content, required type/file/establishment date, and date ordering are
checked before upload. Upload and delete expose operation-local pending and error states.
---
Apartment documents are PDF-only; image uploads belong to condition reports.
## 4. Tenants, Attachments & Transfers
## 4. Tenants and attachments
- **Angular components**: `TenantsPageComponent`, `TenantAttachmentTransferService`
- **HTTP Endpoints**:
- **Angular**: `TenantForm`, `TenantsPage`, `TenantAttachmentTransferService`
- **Mutable commands**:
- `POST /api/tenants`
- `PUT /api/tenants/{id}`
- `POST /api/tenants/{id}/attachments` (multipart)
- `DELETE /api/tenants/{id}/attachments/{attachmentId}`
- `POST /api/tenants/{id}/attachments/{attachmentId}/transfer`
- **Backend Controller**: `TenantController`
- **Request DTO / Invariants**:
- `firstName`, `lastName`, `email`, `phone`, `address`: `@NotBlank` (address is mandatory)
- `attachment`: non-empty file
- `targetTenantId`: `@NotBlank`, must exist
- **Error Mapping**:
- `address` `not_blank`: "L'adresse est obligatoire."
- `email` `invalid`: "L'adresse e-mail est invalide."
- `targetTenantId` `not_found`: "Le locataire cible est introuvable."
- **Interaction Contract**:
- Action-local pending states (transferring/deleting)
- Retained form state on submission error
- `PUT /api/tenants/{tenantId}`
- `DELETE /api/tenants/{tenantId}`
- `POST /api/tenants/{tenantId}/attachments` (multipart)
- `DELETE /api/tenants/{tenantId}/attachments/{attachmentId}`
- **Backend**: `TenantController`, `TenantApi.Request`
- **Authoritative constraints**:
- `firstName`, `lastName`, `email`, and `address` are `@NotBlank`.
- `email` is `@Email`.
- `phone` is optional; when present it must satisfy the shared `PhoneNumber` rule.
- Attachment `file` is required and non-empty. Its content type is not restricted to PDF, and its
size is bounded by `lmnp.attachments.max-size` (250 MB by default).
- **Angular mirror**: required identity/address fields, email, and optional-phone syntax are checked
before save. Tenant values survive a failed request. Attachment upload reports per-tenant
progress; upload, download, and delete use action-local pending/error state.
---
`TenantAttachmentTransferService` coordinates upload, download, and delete state; there is no
attachment-transfer endpoint or `targetTenantId` request.
## 5. Lease Draft, Amendments & Custom Clauses
## 5. Lease drafts, amendments, clauses, and document revisions
- **Angular components**: `LeaseFormComponent`, `LeaseAmendmentFormComponent`, `LeaseClauseEditorComponent`, `LeaseLegalTermsComponent`
- **HTTP Endpoints**:
- **Angular**: `LeaseForm`, `GuarantorForm`, `LeaseClauseEditor`, `LeaseLegalTerms`,
`LeaseAmendmentForm`, `LeasesPage`
- **Mutable commands**:
- `POST /api/leases`
- `PUT /api/leases/{id}`
- `POST /api/leases/{id}/amendments`
- **Backend Controller**: `LeaseController`
- **Request DTO / Invariants**:
- `apartmentId`, `tenantIds`: non-empty list of valid IDs
- `startDate`, `endDate`: `startDate` <= `endDate`
- `monthlyRent`, `monthlyCharges`, `securityDeposit`: >= 0
- `amendmentDate`, `effectiveDate`: valid ISO dates
- **Error Mapping**:
- `tenantIds` `empty`: "Sélectionnez au moins un locataire."
- `monthlyRent` `min`: "Le loyer doit être positif."
- `endDate` `before_start`: "La date de fin doit être postérieure à la date de début."
- `LeaseRuleViolation` (400 Bad Request): Specific French domain guidance.
- **Interaction Contract**:
- Non-blocking warning banners for non-fatal overlap conditions
- Multi-step dialog validation and preserved draft values
- `PUT /api/leases/{leaseId}`
- `DELETE /api/leases/{leaseId}`
- `POST /api/leases/{leaseId}/amendments`
- `POST /api/leases/{leaseId}/revisions`
- **Backend**: `LeaseController`, `LeaseApi.DraftRequest`, `LeaseApi.AmendmentRequest`,
`LeaseNestedRequestValidator`
- **Authoritative draft constraints**:
- `apartmentId` is non-blank and `tenantIds` is a non-empty list of distinct, non-blank IDs.
- `startDate`, `monthlyRent`, `monthlyCharges`, and `securityDeposit` are non-null.
- Rent is strictly positive; charges and deposit are positive or zero; `dueDay` is 1 through 28.
- Guarantor first name, last name, and email are required; email syntax is validated.
- At most 20 special clauses and 20 parameters per clause are accepted. Parameter values are at
most 500 characters, custom clause text 4,000, supplement justification 2,000, IRL series ID
100, recent-work description 4,000, and other terms 8,000.
- Domain services verify referenced resources, lease-type/legal-term coherence, special-clause
catalog rules, and occupancy conflicts.
- **Authoritative amendment constraints**:
- `effectiveDate`, IANA `timeZone`, distinct non-empty `tenantIds`, `endDate`, `monthlyRent`, and
`monthlyCharges` are required.
- `effectiveDate` cannot be in the past; `endDate` cannot precede it.
- Amount and due-day constraints match the draft request.
- **Angular mirror**: important required fields, amount/day bounds, dates, guarantor contact data,
clause inputs, and conditional legal terms are checked before submit. Known nested server paths
are routed to the core form, guarantor, legal-terms, or clause editor without clearing the draft.
- **Document revision command**: generating a revision has no editable request payload; its
operation-local pending/error state is separate from lease editing and downloads.
---
## 6. Furniture inventory
## 6. Furniture Inventory
- **Angular**: `InventoryPage`, `InventoryLineCard`, `inventoryLineErrors`
- **Mutable command**: `PUT /api/leases/{leaseId}/inventory`
- **Backend**: `FurnitureInventoryController`, `FurnitureInventoryRequestValidator`
- **Authoritative constraints**:
- `version` and `lines` are required; version is non-negative.
- Every line requires a unique non-blank ID, valid apartment `privateAreaId`, non-blank
designation, equipment kind, and non-negative integer quantity.
- A regulatory category is unique. Its equipment classification cannot change; present,
absent, and undeclared quantities must agree with presence.
- **Angular mirror**: the same editable line constraints produce indexed errors such as
`lines[0].quantity`; the first invalid control is focused and editing clears that line's server
error. Save is disabled only for the active inventory request.
- **Angular components**: `InventoryPageComponent`, `InventoryLineValidation`
- **HTTP Endpoints**:
- `GET /api/leases/{leaseId}/inventory`
- `PUT /api/leases/{leaseId}/inventory`
- **Backend Controller**: `FurnitureInventoryController`
- **Request DTO / Invariants**:
- `lines[i].privateAreaId`: must exist in apartment
- `lines[i].designation`: `@NotBlank` when `presence` == `PRESENT`
- `lines[i].quantity`: >= 0
- **Error Mapping**:
- `lines[i].designation` `not_blank`: "La désignation est obligatoire."
- `lines[i].quantity` `min`: "La quantité doit être supérieure ou égale à 0."
- **Interaction Contract**:
- Inline category status badges & missing element indicators
- Immediate validation highlight on line value change
- Version concurrency conflict (409) prompts reload without losing edits
## 7. Lease finalization and guarantee deeds
---
## 7. Lease Finalization & Guarantee Deeds
- **Angular components**: `LeaseFinalizationPageComponent`, `LeaseFinalizationFormComponent`, `GuaranteeDeedListComponent`
- **HTTP Endpoints**:
- **Angular**: `LeaseFinalizationPage`, `LeaseFinalizationForm`, `GuaranteeDeedList`
- **Mutable commands**:
- `POST /api/leases/{leaseId}/guarantee-deeds` (multipart)
- `POST /api/leases/{leaseId}/finalization` (multipart)
- **Backend Controller**: `LeaseFinalizationController`
- **Request DTO / Invariants**:
- `signedLease`: valid non-empty PDF file
- `lastSignatureDate`: `@NotNull`, <= today
- `allPartiesSigned`: `@AssertTrue`
- `guarantorId`, `tenantId`: required for guarantee deed
- **Error Mapping**:
- `signedLease` `required`: "Le scan du bail signé est obligatoire."
- `allPartiesSigned` `required`: "Toutes les parties doivent avoir signé."
- `LeaseFinalizationConflict` (409 Conflict): "Bail déjà finalisé."
- **Interaction Contract**:
- Summary check of mandatory annexes and signatures before final submission
- File size and MIME type pre-checks
- **Backend**: `LeaseFinalizationController`, `LeaseFinalizationApi`
- **Guarantee-deed multipart contract**: optional request parameters are validated together as
`requirementIndex`, `guarantorId`, `tenantId`, and true `attestation`; `file` is a required,
valid, non-empty PDF.
- **Finalization multipart contract**:
- `signedLease` is a required valid PDF.
- JSON part `metadata` contains positive `revisionNumber`, `lastSignatureDate`,
`allPartiesSigned`, `revisionMatches`, `confirmedWarningCodes`,
`entryInventoryDiscrepancies`, `inventoryCorrections`, and
`allInventoryCorrectionsInitialed`.
- Boolean attestations and inventory corrections must be internally consistent; the service
checks signature date, required deeds, warnings, and revision identity.
- PDF size uses `lmnp.attachments.max-size` (250 MB by default).
- **Angular mirror**: file validity, attestations, missing deeds, warning confirmations, and
conditional inventory corrections are checked before the confirmation dialog. Upload and final
submission use separate pending/error state and preserve correction input after failure.
---
## 8. Lease ending, evidence, and resolution
## 8. Lease Ending, Evidence & Resolutions
- **Angular components**: `LeaseEndPageComponent`, `LeaseEndNoticeCardComponent`, `LeaseEndResolutionFormsComponent`
- **HTTP Endpoints**:
- **Angular**: `LeaseEndPage`, `LeaseEndNoticeForm`, `LeaseEndNoticeCard`,
`LeaseEndResolutionForms`
- **Mutable commands**:
- `POST /api/leases/{leaseId}/end/notices` (multipart)
- `POST /api/leases/{leaseId}/end/notices/{noticeId}/evidence` (multipart)
- `POST /api/leases/{leaseId}/end/notices/{noticeId}/withdrawals`
- `POST /api/leases/{leaseId}/end/notices/{noticeId}/invalidations`
- `POST /api/leases/{leaseId}/end/agreed-early-end`
- `POST /api/leases/{leaseId}/end/closure`
- **Backend Controller**: `LeaseEndingController`
- **Request DTO / Invariants**:
- `receivedOn`, `requestedDepartureOn`: `receivedOn` <= `requestedDepartureOn`
- `reason`: `@NotBlank` for withdrawals, invalidations, agreed ends
- `confirmed`: `@AssertTrue` for closure
- **Error Mapping**:
- `receivedOn` `not_future`: "La date de réception ne peut pas être dans le futur."
- `requestedDepartureOn` `after_or_equal_received_on`: "La date de départ doit être supérieure ou égale à la réception."
- `reason` `required`: "Le motif est obligatoire."
- **Interaction Contract**:
- Dynamic form visibility based on selected resolution type
- Clear confirmation feedback and preserved notice history
- **Authoritative constraints**:
- A notice supplies IANA `timeZone`, `receivedOn`, `requestedDepartureOn`, delivery mode, and the
applicable confirmations. Receipt cannot be in the future and departure cannot precede it.
- Evidence is optional only with explicit missing-evidence confirmation; uploaded evidence is
non-empty and bounded by the configured attachment limit.
- Withdrawal, invalidation, and agreed-end reasons are non-blank and at most 500 characters.
- Closure requires a valid time zone and true `confirmed`.
- Domain rules enforce the lease phase, notice history, eligible dates, and exit-report
prerequisites.
- **Angular mirror**: conditional delivery/evidence fields, dates, reasons, and closure confirmation
are validated where their controls live. Each notice/action key has independent confirmation,
pending, structured field-error, and operation-error state.
---
## 9. State of the premises
## 9. State-of-the-Premises (Condition Report)
- **Angular components**: `ConditionReportPageComponent`, `ConditionReportEditorComponent`, `ConditionReportDetailsSectionComponent`, `ConditionReportInventorySectionComponent`, `ConditionReportEvidenceSectionComponent`
- **HTTP Endpoints**:
- **Angular**: `ConditionReportPage`, `ConditionReportEditor`,
`ConditionReportDetailsSection`, `ConditionReportInventorySection`,
`ConditionReportEvidenceSection`
- **Mutable commands**:
- `PUT /api/leases/{leaseId}/condition-reports/{type}`
- `POST /api/leases/{leaseId}/condition-reports/{type}/photos` (multipart)
- `DELETE /api/leases/{leaseId}/condition-reports/{type}/photos/{reference}?version=...`
- `POST /api/leases/{leaseId}/condition-reports/{type}/revisions?version=...`
- `POST /api/leases/{leaseId}/condition-reports/{type}/finalization` (multipart)
- `POST /api/leases/{leaseId}/condition-reports/{type}/delivery-attestations`
- `POST /api/leases/{leaseId}/condition-reports/{type}/supplements` (multipart)
- **Backend Controller**: `PropertyConditionReportController`
- **Request DTO / Invariants**:
- `occurredOn`, `timeZone`: `@NotNull`, `@NotBlank`
- `participants[i]`: `@NotBlank partyId`, `@NotBlank name`
- `observations[i]`: degraded states (`DEGRADE`, `ABSENT`, `NON_APPLICABLE`) require non-blank `observation`
- `meterReadings[i]`: `index` >= 0 when `RECORDED`
- `inventoryConditions[i]`: split sum must equal contractual quantity
- `photos` / `zonePhotoAbsences`: photo or declared absence reason for every zone
- Exit report: every entry photo requires exit photo or absence reason
- **Error Mapping**:
- `validation_failed`: French inline field messages mapped to section components
- Photo upload size / format error: "La photo est trop volumineuse ou invalide."
- **Interaction Contract**:
- Section-by-section validation summary
- First-invalid control auto-scroll/focus
- Local image preview and graceful upload failure isolation
- Shared state-of-the-premises decomposition aligned with Issue #40
- `POST /api/leases/{leaseId}/condition-reports/ENTRY/supplements` (multipart)
- **Draft authority**:
- Non-negative `version`, `occurredOn`, IANA `timeZone`, and every nested list are required.
- Participants, zones, observations, meters, access means, inventory splits, photo absences, and
entry-photo dispositions have explicit required/type/non-negative constraints.
- The draft validator enforces conditional observations, represented-party details, forwarding
addresses on exit, and exact inventory split totals. Finalization readiness separately requires
one photo or audited absence per zone and, for exit reports, a disposition for every entry
photo.
- **Photo multipart contract**:
- Required non-blank `zoneId` parameter and required `file` part.
- Supported content: JPEG, PNG, WebP, HEIC, or HEIF, checked against both MIME type and file
signature.
- The configurable photo limit defaults to 40 MB and is returned by the report response for
frontend pre-checks.
- **Finalization multipart contract**: required valid PDF `file` plus JSON `metadata` containing a
non-negative `version` and true `attestation` and `allPartiesSigned`; the PDF limit defaults to
250 MB and is also returned by the report response.
- **Supplement multipart contract**: entry reports only; required valid PDF `signedPdf`, optional
photo parts, and JSON `metadata` containing matching `photoZoneIds`, `requestedOn`,
non-blank `acceptedAdditions`, `supplementType`, `lateWarningConfirmed`, optional
`heatingPeriodStartedOn`, and optional `reason`. The request date cannot precede the report or be
in the future; a heating-period start cannot follow the request. Photo/PDF limits match the
upload types above.
- **Angular mirror**: the editor checks the important nested draft rules before save; evidence
controls pre-check types and response-provided limits. Structured server paths are mapped to the
owning section, preserve the draft, focus the first correction, and clear independently.
Saving, photo work, revision generation, finalization, delivery attestation, supplement upload,
comparison loading, and downloads use separate busy states.
---
This is the Issue #40 overlap: the existing decomposed condition-report editor and section
components own the UI; Issue #59 adds no duplicate state-of-the-premises implementation.
## 10. Rent Payments, Allocations, Reversals & Refunds
## 10. Rent payments, allocations, reversal, and refunds
- **Angular components**: `RentPaymentValidationDialogComponent`, `RentPaymentAllocationDialogComponent`, `RentPaymentReversalDialogComponent`, `RentPaymentRefundDialogComponent`
- **HTTP Endpoints**:
- **Angular**: `RentPaymentValidationDialog`, `RentPaymentAllocationDialog`,
`RentPaymentReversalDialog`, `RentPaymentRefundDialog`, `RentDueCorrection`
- **Mutable commands**:
- `POST /api/rent-dues/refresh`
- `POST /api/rent-payments`
- `POST /api/rent-payments/{paymentId}/allocations`
- `POST /api/rent-payments/{paymentId}/reversals`
- `POST /api/rent-payments/{paymentId}/reversal`
- `POST /api/rent-payments/{paymentId}/refunds`
- **Backend Controller**: `RentPaymentController`
- **Request DTO / Invariants**:
- `amount`: `@NotNull`, `@DecimalMin("0.01")`
- `receivedAt` / `refundedAt`: `@NotNull`, not future date
- `timeZone`: valid IANA time zone identifier
- `reason`: `@NotBlank` for reversals
- Allocation/refund amount <= remaining/unallocated balance
- **Error Mapping**:
- `receivedAt` `future_date`: "La date de réception ne peut pas être dans le futur."
- `amount` `exceeds_unallocated_balance`: "Le montant dépasse le solde non affecté."
- `timeZone` `iana_time_zone`: "Fuseau horaire invalide."
- `ConcurrentChange` (409 Conflict): "La ressource a été modifiée ; rechargez-la puis réessayez."
- **Interaction Contract**:
- Real-time calculation of remaining balance and surplus
- Modal focus trap and escape cancellation handling
- Disable action button during active HTTP submission
- **Backend**: `RentDueController`, `RentPaymentController`, `RentPaymentApi`
- **Authoritative constraints**:
- Record: non-blank operation/due IDs and time zone, non-negative due version, required
`receivedAt`, and strictly positive amount.
- Allocation: non-blank operation/due IDs, non-negative payment/due versions, and strictly
positive amount.
- Reversal: non-negative payment version, non-blank reason of at most 500 characters, and
non-blank time zone.
- Refund: non-blank operation ID/time zone, non-negative payment version, required
`refundedAt`, and strictly positive amount.
- Application rules reject future receipt/refund dates, cross-lease or duplicate allocations,
and amounts exceeding payment, due, or unallocated balances.
- **Angular mirror**: dialogs validate the editable date, amount, due, and reason fields, display
known server guidance inline, focus the first invalid control, and disable only their active
submit. Stale/concurrent responses prompt data reconciliation without silently accepting a
payment.
---
The reversal route is singular. Amount DTOs use `@Positive`, not `@DecimalMin("0.01")`.
## 11. Rent Revisions (Preview, Proposal & Application)
## 11. Rent revisions
- **Angular components**: `RentRevisionPreviewComponent`, `RentRevisionApplicationComponent`, `RentRevisionProposalsComponent`
- **HTTP Endpoints**:
- **Angular**: `RentRevisionPreviewComponent`, `RentRevisionProposalsComponent`,
`RentRevisionApplicationComponent`
- **Mutable commands**:
- `POST /api/leases/{leaseId}/rent-revisions/preview`
- `POST /api/leases/{leaseId}/rent-revisions`
- `POST /api/leases/{leaseId}/rent-revisions/{revisionId}/apply` (multipart)
- **Backend Controller**: `RentRevisionController`
- **Request DTO / Invariants**:
- `sentOn`: valid ISO date, not in future
- `deliveryMethod`: valid enum (`REGISTERED_LETTER`, `POSTAL_LETTER`, etc.)
- `deliveryConfirmed`: `@NotNull` boolean
- `proof`: optional PDF proof file
- `timeZone`: valid IANA time zone
- **Error Mapping**:
- `sentOn` `future`: "La date d'envoi ne peut pas être dans le futur."
- `deliveryMethod` `invalid`: "Mode de délivrance invalide."
- `IrlIndexUnavailable` (533 Service Unavailable): "L'indice IRL n'est pas disponible."
- **Interaction Contract**:
- Immediate preview updating on parameter modification
- Local error state display with clear actionable guidance
- **Backend**: `RentRevisionController`, `RentRevisionApi`
- **Authoritative constraints**:
- Preview requires IANA `timeZone`; optional series override is at most 9 characters and manual
indices are strictly positive.
- Proposal requires time zone, `requestedOn`, strictly positive chosen rent, non-negative chosen
charges when present, and non-blank warning codes.
- Application multipart parameters are `timeZone`, `sentOn`, `deliveryMethod`, and
`deliveryConfirmed`; optional `proof` must be a valid PDF within the attachment limit.
- Policy/domain rules enforce request windows, supported IRL series, confirmation of overrides,
ceilings, delivery date, and idempotent application.
- **Angular mirror**: manual-index confirmation and positivity, proposal values/warnings, delivery
mode/confirmation, and PDF proof are validated locally. Preview, proposal, application, and
downloads each expose their own pending/error state.
- **Unavailable index contract**: `IRL_INDEX_UNAVAILABLE` is HTTP 503 Service Unavailable and
switches the preview to the guided manual-index path.
---
## 12. Session command
## Verification & Compliance Summary
- **Angular**: `SessionService`, settings logout action
- **Mutable command**: `POST /logout`
- **Payload validation**: none.
- **Interaction**: duplicate logout requests are prevented while pending; failure restores the
available action with a safe visible error.
1. **Structured Failure Preservation**: All controllers throw typed domain/validation exceptions caught by `@RestControllerAdvice` or local `@ExceptionHandler` methods, producing top-level `"code": "validation_failed"` JSON payloads with field-path arrays. Raw Jakarta annotations or stack traces are never exposed to clients.
2. **French Localization**: Field errors are translated to natural French inline messages within Angular components using `saveFailure()` and `httpErrorMessage()`.
3. **Obsolete Helpers Cleaned**: All legacy string error parsers and duplicated catch blocks have been replaced with the standardized `SaveFailure` and `FieldValidationError` contract.
4. **Consistency**: Touch-or-submit timing, required markers (`*`), first-invalid focus, independent error clearing, preserved inputs, and pending spin states are uniformly enforced across all 11 domain areas.
5. **No Regressions**: Full frontend (unit + e2e) and backend (unit + integration + static check) test suites pass cleanly.
## Evidence and verification gates
Source-level evidence is kept beside behavior in focused frontend and backend tests. The critical
browser journey in `frontend/e2e/app.spec.ts` verifies private-area client blocking, structured
indexed server guidance, first-invalid focus, independent clearing, preserved values, and a
successful retry. The same suite exercises profile completion, tenant/attachment creation, lease
creation, rent-payment recording, and the native condition-photo control.
Local verification recorded on 2026-07-26:
- Prettier passed for every frontend/documentation file changed since the epic baseline.
- Frontend lint, application/e2e type checking, production build, and all 327 unit tests passed.
- All 36 Playwright journeys passed across mobile, tablet, and desktop viewports.
- `./mvnw spotless:check verify` passed: 415 unit/web tests, 16 Docker-backed integration tests,
packaging, and coverage checks.