From b9e99348f72bfaa17cb1e137ef499ee2fc68987d Mon Sep 17 00:00:00 2001
From: Nessar
Date: Sun, 19 Jul 2026 00:49:10 +0200
Subject: [PATCH] feat: add rent payment history and reversal dialogs
- Implemented RentPaymentHistoryServiceTest to validate payment retrieval with refunds and reversals.
- Created ReverseRentPaymentServiceTest to ensure correct handling of payment reversals and allocation releases.
- Developed rent-payment-allocation-dialog component with form for surplus allocation.
- Added rent-payment-refund-dialog component for processing refunds with validation.
- Introduced rent-payment-reversal-dialog component for reversing payments with reason input.
- Implemented utility functions for formatting Euro values and handling date formatting.
- Enhanced rent-payment-dialogs.spec.ts to cover new dialog functionalities and error handling.
---
.vscode/launch.json | 89 +++---
CONTEXT.md | 10 +-
.../RentalApplicationConfiguration.java | 51 ++--
.../web/common/RentalApiExceptionHandler.java | 3 +-
.../adapter/in/web/rent/RentPaymentApi.java | 42 ++-
.../in/web/rent/RentPaymentController.java | 151 +++-------
.../MongoRentPaymentRefundRepository.java | 31 +--
.../rent/MongoRentPaymentRepository.java | 14 -
.../port/out/RentalDocumentRenderer.java | 3 +-
.../rent/AllocateRentPaymentService.java | 35 ++-
.../rent/ConfirmRentPaymentRefundService.java | 40 ---
.../rent/ConfirmRentPaymentService.java | 74 -----
.../rent/GeneratePaymentReceiptService.java | 5 +-
.../rent/RecordRentPaymentService.java | 85 ++++++
.../rent/RefundRentPaymentService.java | 71 +++++
.../application/rent/RentPaymentConflict.java | 7 +
.../rent/RentPaymentDraftService.java | 71 -----
.../application/rent/RentPaymentHistory.java | 18 ++
.../rent/RentPaymentHistoryService.java | 57 ++++
.../rent/RentPaymentRefundDraftService.java | 66 -----
.../rent/ReverseRentPaymentService.java | 24 +-
.../rental/domain/rent/RentPayment.java | 43 +--
.../domain/rent/RentPaymentConfirmed.java | 16 --
.../rental/domain/rent/RentPaymentRefund.java | 55 +---
.../rent/RentPaymentRefundRepository.java | 4 +-
.../domain/rent/RentPaymentRefundStatus.java | 6 -
.../domain/rent/RentPaymentRefunded.java | 12 -
.../domain/rent/RentPaymentRepository.java | 2 -
.../domain/rent/RentPaymentReversal.java | 20 +-
.../rental/domain/rent/RentPaymentStatus.java | 1 -
.../templates/rental/payment-receipt.html | 5 +-
.../RentalApplicationConfigurationTest.java | 14 +
.../adapter/in/web/RentalWebAdaptersTest.java | 128 +++++----
.../out/mongodb/RentalMongoTransactionIT.java | 10 +-
.../rent/MongoRentRepositoriesTest.java | 35 +--
.../ThymeleafRentalDocumentRendererTest.java | 2 +-
.../rent/AllocateRentPaymentServiceTest.java | 133 +++++++++
.../rent/RecordRentPaymentServiceTest.java | 147 ++++++++++
.../rent/RefundRentPaymentServiceTest.java | 122 ++++++++
.../rent/RentMoneyMovementServicesTest.java | 187 -------------
.../rent/RentPaymentDraftServiceTest.java | 135 ---------
.../rent/RentPaymentHistoryServiceTest.java | 80 ++++++
.../RentPaymentRefundDraftServiceTest.java | 140 ----------
.../RentalDocumentGenerationServicesTest.java | 13 +-
.../rent/ReverseRentPaymentServiceTest.java | 128 +++++++++
.../rent/RentPaymentCorrectionTest.java | 45 ++-
.../rental/domain/rent/RentPaymentTest.java | 34 ++-
.../ReplacementRentalApiJourneyIT.java | 132 +++++----
docs/spec/lmnp-organizer-prd.md | 2 +-
frontend/e2e/app.spec.ts | 131 ++++++++-
.../rent-payment-allocation-dialog.css | 41 +++
.../rent-payment-allocation-dialog.html | 49 ++++
.../rent-payment-allocation-dialog.ts | 131 +++++++++
.../components/rent-payment-dialog-result.ts | 9 +
.../components/rent-payment-dialogs.spec.ts | 171 ++++++++++++
.../rent-payment-refund-dialog.css | 33 +++
.../rent-payment-refund-dialog.html | 39 +++
.../rent-payment-refund-dialog.ts | 86 ++++++
.../rent-payment-reversal-dialog.css | 45 +++
.../rent-payment-reversal-dialog.html | 41 +++
.../rent-payment-reversal-dialog.ts | 61 ++++
.../rent-payment-validation-dialog.css | 72 ++++-
.../rent-payment-validation-dialog.html | 71 ++++-
.../rent-payment-validation-dialog.ts | 82 +++++-
.../rent-payments-page/rent-payments-page.css | 150 ++++++++--
.../rent-payments-page.html | 199 ++++++++++++-
.../rent-payments-page.spec.ts | 261 ++++++++++++------
.../rent-payments-page/rent-payments-page.ts | 144 +++++++++-
.../src/app/services/lease.service.spec.ts | 4 +
frontend/src/app/services/lease.service.ts | 6 +
.../src/app/services/rent-payment-format.ts | 14 +
.../src/app/services/rent-payment-schedule.ts | 29 --
.../app/services/rent-payment.service.spec.ts | 37 ++-
.../src/app/services/rent-payment.service.ts | 81 +++++-
.../app/services/workspace.service.spec.ts | 42 +--
.../src/app/services/workspace.service.ts | 45 ++-
76 files changed, 3141 insertions(+), 1531 deletions(-)
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentRefundService.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentService.java
create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RecordRentPaymentService.java
create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RefundRentPaymentService.java
create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentConflict.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentDraftService.java
create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistory.java
create mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistoryService.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentRefundDraftService.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentConfirmed.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundStatus.java
delete mode 100644 backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefunded.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/adapter/in/config/RentalApplicationConfigurationTest.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/AllocateRentPaymentServiceTest.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RecordRentPaymentServiceTest.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RefundRentPaymentServiceTest.java
delete mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RentMoneyMovementServicesTest.java
delete mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentDraftServiceTest.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistoryServiceTest.java
delete mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentRefundDraftServiceTest.java
create mode 100644 backend/src/test/java/fr/nessar/lmnporganizer/rental/application/rent/ReverseRentPaymentServiceTest.java
create mode 100644 frontend/src/app/components/rent-payment-allocation-dialog/rent-payment-allocation-dialog.css
create mode 100644 frontend/src/app/components/rent-payment-allocation-dialog/rent-payment-allocation-dialog.html
create mode 100644 frontend/src/app/components/rent-payment-allocation-dialog/rent-payment-allocation-dialog.ts
create mode 100644 frontend/src/app/components/rent-payment-dialog-result.ts
create mode 100644 frontend/src/app/components/rent-payment-dialogs.spec.ts
create mode 100644 frontend/src/app/components/rent-payment-refund-dialog/rent-payment-refund-dialog.css
create mode 100644 frontend/src/app/components/rent-payment-refund-dialog/rent-payment-refund-dialog.html
create mode 100644 frontend/src/app/components/rent-payment-refund-dialog/rent-payment-refund-dialog.ts
create mode 100644 frontend/src/app/components/rent-payment-reversal-dialog/rent-payment-reversal-dialog.css
create mode 100644 frontend/src/app/components/rent-payment-reversal-dialog/rent-payment-reversal-dialog.html
create mode 100644 frontend/src/app/components/rent-payment-reversal-dialog/rent-payment-reversal-dialog.ts
create mode 100644 frontend/src/app/services/rent-payment-format.ts
delete mode 100644 frontend/src/app/services/rent-payment-schedule.ts
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9a8c28f..e396348 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,44 +1,47 @@
{
- "version": "0.2.0",
- "configurations": [
- {
- "type": "java",
- "name": "Backend: Spring Boot",
- "request": "launch",
- "presentation": {
- "hidden": true
- },
- "mainClass": "fr.nessar.lmnporganizer.backend.BackendApplication",
- "projectName": "backend",
- "cwd": "${workspaceFolder}/backend",
- "envFile": "${workspaceFolder}/.env",
- "env": {
- "SPRING_DATA_MONGODB_URI": "mongodb://admin:adminpassword@localhost:27017/lmnp?authSource=admin"
- },
- "preLaunchTask": "Start MongoDB",
- "postDebugTask": "Stop MongoDB"
- },
- {
- "type": "chrome",
- "name": "Frontend: Angular",
- "request": "launch",
- "presentation": {
- "hidden": true
- },
- "url": "http://localhost:4200",
- "webRoot": "${workspaceFolder}/frontend",
- "sourceMaps": true,
- "preLaunchTask": "Start Angular"
- }
- ],
- "compounds": [
- {
- "name": "Debug whole app",
- "configurations": ["Backend: Spring Boot", "Frontend: Angular"],
- "stopAll": true,
- "presentation": {
- "order": 1
- }
- }
- ]
-}
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Backend: Spring Boot",
+ "request": "launch",
+ "presentation": {
+ "hidden": true
+ },
+ "mainClass": "fr.nessar.lmnporganizer.LmnpOrganizerApplication",
+ "projectName": "backend",
+ "cwd": "${workspaceFolder}/backend",
+ "envFile": "${workspaceFolder}/.env",
+ "env": {
+ "SPRING_DATA_MONGODB_URI": "mongodb://admin:adminpassword@localhost:27017/lmnp?authSource=admin"
+ },
+ "preLaunchTask": "Start MongoDB",
+ "postDebugTask": "Stop MongoDB"
+ },
+ {
+ "type": "chrome",
+ "name": "Frontend: Angular",
+ "request": "launch",
+ "presentation": {
+ "hidden": true
+ },
+ "url": "http://localhost:4200",
+ "webRoot": "${workspaceFolder}/frontend",
+ "sourceMaps": true,
+ "preLaunchTask": "Start Angular"
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Debug whole app",
+ "configurations": [
+ "Backend: Spring Boot",
+ "Frontend: Angular"
+ ],
+ "stopAll": true,
+ "presentation": {
+ "order": 1
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CONTEXT.md b/CONTEXT.md
index d283b66..6228a3a 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -33,11 +33,11 @@ Loyer mensuel créé uniquement à l'activation d'un bail, généré à partir d
_Avoid_: Paiement, mensualité
**Règlement de loyer** (Code: `RentPayment`):
-Encaissement daté d'un montant strictement positif pouvant être affecté en tout ou partie à des échéances de loyer, sans que la somme de ses affectations dépasse le montant reçu ; le surplus reste non affecté jusqu'à son report, son remboursement ou un autre traitement. Un règlement en brouillon est modifiable ou supprimable, tandis qu'un règlement confirmé est immuable et ne peut être corrigé que par contrepassation puis remplacement.
-_Avoid_: Échéance, mensualité
+Encaissement déjà reçu, donc jamais daté dans le futur selon le calendrier UTC, et d'un montant strictement positif, pouvant être affecté en tout ou partie à des échéances de loyer sans que la somme de ses affectations dépasse le montant reçu ; le surplus reste non affecté jusqu'à son report, son remboursement ou un autre traitement. Un règlement est enregistré comme un fait confirmé et immuable qui ne peut être corrigé que par contrepassation puis remplacement.
+_Avoid_: Échéance, mensualité, marquage manuel comme payé
**Contrepassation de règlement** (Code: `RentPaymentReversal`):
-Écriture immuable et consultable qui annule les effets d'un règlement confirmé sans effacer le fait historique ni sa trace d'audit. Ses affectations sont annulées et un éventuel règlement corrigé est enregistré séparément.
+Écriture immuable, motivée et consultable qui annule les effets d'un règlement confirmé sans effacer le fait historique ni sa trace d'audit. Ses affectations sont annulées, ses remboursements déjà confirmés restent des sorties historiques immuables, et un éventuel règlement corrigé est enregistré séparément.
_Avoid_: Suppression, modification, correction silencieuse
**Quittance de loyer** (Code: `RentReceipt`):
@@ -49,11 +49,11 @@ Document attestant le montant total d'un règlement confirmé et détaillant ses
_Avoid_: Quittance de loyer
**Remboursement de trop-perçu** (Code: `RentPaymentRefund`):
-Restitution positive de tout ou partie du montant non affecté d'un règlement confirmé, sans jamais dépasser son solde non affecté. Une fois confirmé, le remboursement est immuable afin de préserver la trace d'audit.
+Restitution déjà effectuée, donc jamais datée dans le futur selon le calendrier UTC, de tout ou partie du montant non affecté d'un règlement confirmé, sans jamais dépasser son solde non affecté. Un remboursement est enregistré comme un fait confirmé et immuable afin de préserver la trace d'audit.
_Avoid_: Suppression du règlement, affectation négative
**Affectation de règlement** (Code: `PaymentAllocation`):
-Part d'un règlement attribuée à une échéance de loyer sans dépasser le solde restant de celle-ci. Un règlement peut être réparti entre plusieurs échéances d'un même bail, mais jamais entre plusieurs baux.
+Part d'un règlement attribuée à une échéance de loyer non réglée ou partiellement réglée, sans dépasser le solde restant de celle-ci. Un règlement peut être réparti entre plusieurs échéances d'un même bail, mais jamais entre plusieurs baux.
_Avoid_: Règlement, paiement
**Échéancier courant**:
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/config/RentalApplicationConfiguration.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/config/RentalApplicationConfiguration.java
index 7a0bbdf..56c53fe 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/config/RentalApplicationConfiguration.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/config/RentalApplicationConfiguration.java
@@ -15,14 +15,13 @@ import fr.nessar.lmnporganizer.rental.application.port.out.RentalDocumentRendere
import fr.nessar.lmnporganizer.rental.application.port.out.TenantAttachmentStorage;
import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
import fr.nessar.lmnporganizer.rental.application.rent.AllocateRentPaymentService;
-import fr.nessar.lmnporganizer.rental.application.rent.ConfirmRentPaymentRefundService;
-import fr.nessar.lmnporganizer.rental.application.rent.ConfirmRentPaymentService;
import fr.nessar.lmnporganizer.rental.application.rent.GeneratePaymentReceiptService;
import fr.nessar.lmnporganizer.rental.application.rent.GenerateRentReceiptService;
+import fr.nessar.lmnporganizer.rental.application.rent.RecordRentPaymentService;
+import fr.nessar.lmnporganizer.rental.application.rent.RefundRentPaymentService;
import fr.nessar.lmnporganizer.rental.application.rent.ReverseRentPaymentService;
-import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentDraftService;
-import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentRefundDraftService;
import fr.nessar.lmnporganizer.rental.application.rent.RentDueQueryService;
+import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentHistoryService;
import fr.nessar.lmnporganizer.rental.domain.apartment.ApartmentRepository;
import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordRepository;
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseRepository;
@@ -43,7 +42,7 @@ import java.time.Clock;
public class RentalApplicationConfiguration {
@Bean
Clock rentalClock() {
- return Clock.systemDefaultZone();
+ return Clock.systemUTC();
}
@Bean
@@ -112,32 +111,17 @@ public class RentalApplicationConfiguration {
}
@Bean
- RentPaymentDraftService rentPaymentDraftService(LeaseRepository leases, RentPaymentRepository payments,
- IdentifierGenerator identifiers) {
- return new RentPaymentDraftService(leases, payments, identifiers);
- }
-
- @Bean
- RentPaymentRefundDraftService rentPaymentRefundDraftService(RentPaymentRepository payments,
- RentPaymentRefundRepository refunds,
- IdentifierGenerator identifiers) {
- return new RentPaymentRefundDraftService(payments, refunds, identifiers);
- }
-
- @Bean
- ConfirmRentPaymentService confirmRentPaymentService(RentPaymentRepository payments,
- RentDueRepository rentDues,
- TransactionRunner transactions,
- IdentifierGenerator identifiers, Clock rentalClock) {
- return new ConfirmRentPaymentService(payments, rentDues, transactions, identifiers, rentalClock);
+ RecordRentPaymentService recordRentPaymentService(RentPaymentRepository payments, RentDueRepository rentDues,
+ TransactionRunner transactions, Clock rentalClock) {
+ return new RecordRentPaymentService(payments, rentDues, transactions, rentalClock);
}
@Bean
AllocateRentPaymentService allocateRentPaymentService(RentPaymentRepository payments,
RentDueRepository rentDues,
TransactionRunner transactions,
- IdentifierGenerator identifiers, Clock rentalClock) {
- return new AllocateRentPaymentService(payments, rentDues, transactions, identifiers, rentalClock);
+ Clock rentalClock) {
+ return new AllocateRentPaymentService(payments, rentDues, transactions, rentalClock);
}
@Bean
@@ -150,11 +134,18 @@ public class RentalApplicationConfiguration {
}
@Bean
- ConfirmRentPaymentRefundService confirmRentPaymentRefundService(RentPaymentRepository payments,
- RentPaymentRefundRepository refunds,
- TransactionRunner transactions,
- Clock rentalClock) {
- return new ConfirmRentPaymentRefundService(payments, refunds, transactions, rentalClock);
+ RefundRentPaymentService refundRentPaymentService(RentPaymentRepository payments,
+ RentPaymentRefundRepository refunds,
+ TransactionRunner transactions, Clock rentalClock) {
+ return new RefundRentPaymentService(payments, refunds, transactions, rentalClock);
+ }
+
+ @Bean
+ RentPaymentHistoryService rentPaymentHistoryService(RentPaymentRepository payments,
+ RentPaymentRefundRepository refunds,
+ RentPaymentReversalRepository reversals,
+ LeaseRepository leases) {
+ return new RentPaymentHistoryService(payments, refunds, reversals, leases);
}
@Bean
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/common/RentalApiExceptionHandler.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/common/RentalApiExceptionHandler.java
index 0f18175..d1feec5 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/common/RentalApiExceptionHandler.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/common/RentalApiExceptionHandler.java
@@ -1,5 +1,6 @@
package fr.nessar.lmnporganizer.rental.adapter.in.web.common;
+import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentConflict;
import fr.nessar.lmnporganizer.rental.application.shared.AttachmentStorageFailure;
import fr.nessar.lmnporganizer.rental.application.shared.AttachmentTooLarge;
import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
@@ -28,7 +29,7 @@ public class RentalApiExceptionHandler {
return error(HttpStatus.BAD_REQUEST, exception.getMessage());
}
- @ExceptionHandler({ReferencedResource.class, EmailAlreadyUsed.class})
+ @ExceptionHandler({ReferencedResource.class, EmailAlreadyUsed.class, RentPaymentConflict.class})
ResponseEntity conflict(RuntimeException exception) {
return error(HttpStatus.CONFLICT, exception.getMessage());
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentApi.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentApi.java
index 5e21c24..debc667 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentApi.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentApi.java
@@ -1,14 +1,14 @@
package fr.nessar.lmnporganizer.rental.adapter.in.web.rent;
+import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentHistory;
import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocation;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentReversal;
-import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -18,26 +18,21 @@ final class RentPaymentApi {
private RentPaymentApi() {
}
- record CreateRequest(@NotBlank String leaseId, @NotNull LocalDate receivedAt,
+ record CreateRequest(@NotBlank String operationId, @NotBlank String rentDueId,
+ @NotNull @PositiveOrZero Long rentDueVersion, @NotNull LocalDate receivedAt,
@NotNull @Positive BigDecimal amount) {
}
- record ReviseRequest(@NotNull LocalDate receivedAt, @NotNull @Positive BigDecimal amount) {
+ record AllocationRequest(@NotBlank String operationId, @NotNull @PositiveOrZero Long paymentVersion,
+ @NotBlank String rentDueId, @NotNull @PositiveOrZero Long rentDueVersion,
+ @NotNull @Positive BigDecimal amount) {
}
- record AllocationRequest(@NotBlank String rentDueId, @NotNull @Positive BigDecimal amount) {
+ record ReversalRequest(@NotNull @PositiveOrZero Long paymentVersion, @NotBlank @Size(max = 500) String reason) {
}
- record ConfirmRequest(List<@Valid AllocationRequest> allocations) {
- ConfirmRequest {
- allocations = allocations == null ? List.of() : List.copyOf(allocations);
- }
- }
-
- record ReversalRequest(@NotBlank String reason) {
- }
-
- record RefundRequest(@NotNull @Positive BigDecimal amount, @NotNull LocalDate refundedAt) {
+ record RefundRequest(@NotBlank String operationId, @NotNull @PositiveOrZero Long paymentVersion,
+ @NotNull @Positive BigDecimal amount, @NotNull LocalDate refundedAt) {
}
record AllocationResponse(String id, String rentDueId, BigDecimal amount, LocalDate allocatedAt) {
@@ -49,21 +44,24 @@ final class RentPaymentApi {
record Response(String id, String leaseId, LocalDate receivedAt, BigDecimal amount, String status,
List allocations, BigDecimal allocatedAmount, BigDecimal refundedAmount,
- BigDecimal unallocatedBalance, LocalDate confirmedAt, long version) {
- static Response from(RentPayment payment) {
+ BigDecimal unallocatedBalance, LocalDate confirmedAt, long version,
+ List refunds, ReversalResponse reversal) {
+ static Response from(RentPaymentHistory history) {
+ var payment = history.payment();
return new Response(payment.id().value(), payment.leaseId().value(), payment.receivedAt(),
payment.amount(), payment.status().name(),
payment.allocations().stream().map(AllocationResponse::from).toList(), payment.allocatedAmount(),
- payment.refundedAmount(), payment.unallocatedBalance(), payment.confirmedAt(), payment.version());
+ payment.refundedAmount(), payment.unallocatedBalance(), payment.confirmedAt(), payment.version(),
+ history.refunds().stream().map(RefundResponse::from).toList(),
+ history.reversal().map(ReversalResponse::from).orElse(null));
}
}
record RefundResponse(String id, String leaseId, String paymentId, BigDecimal amount, LocalDate refundedAt,
- String status, LocalDate confirmedAt, long version) {
+ long version) {
static RefundResponse from(RentPaymentRefund refund) {
return new RefundResponse(refund.id().value(), refund.leaseId().value(), refund.paymentId().value(),
- refund.amount(), refund.refundedAt(), refund.status().name(), refund.confirmedAt(),
- refund.version());
+ refund.amount(), refund.refundedAt(), refund.version());
}
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentController.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentController.java
index 4440069..d9904d3 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentController.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/in/web/rent/RentPaymentController.java
@@ -2,20 +2,16 @@ package fr.nessar.lmnporganizer.rental.adapter.in.web.rent;
import fr.nessar.lmnporganizer.rental.adapter.in.web.common.AuthenticatedLandlord;
import fr.nessar.lmnporganizer.rental.application.rent.AllocateRentPaymentService;
-import fr.nessar.lmnporganizer.rental.application.rent.ConfirmRentPaymentRefundService;
-import fr.nessar.lmnporganizer.rental.application.rent.ConfirmRentPaymentService;
import fr.nessar.lmnporganizer.rental.application.rent.GeneratePaymentReceiptService;
-import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentDraftService;
-import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentRefundDraftService;
+import fr.nessar.lmnporganizer.rental.application.rent.RecordRentPaymentService;
+import fr.nessar.lmnporganizer.rental.application.rent.RefundRentPaymentService;
+import fr.nessar.lmnporganizer.rental.application.rent.RentPaymentHistoryService;
import fr.nessar.lmnporganizer.rental.application.rent.ReverseRentPaymentService;
import fr.nessar.lmnporganizer.rental.application.shared.GeneratedDocument;
-import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentDueId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundId;
import jakarta.validation.Valid;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
@@ -23,11 +19,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
-import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -38,27 +32,24 @@ import java.util.List;
@RestController("rentalRentPaymentController")
@RequestMapping("/api/rent-payments")
public class RentPaymentController {
- private final RentPaymentDraftService drafts;
- private final ConfirmRentPaymentService confirmation;
+ private final RentPaymentHistoryService history;
+ private final RecordRentPaymentService recording;
private final AllocateRentPaymentService allocation;
+ private final RefundRentPaymentService refunds;
private final ReverseRentPaymentService reversal;
- private final RentPaymentRefundDraftService refundDrafts;
- private final ConfirmRentPaymentRefundService refundConfirmation;
private final GeneratePaymentReceiptService receipts;
private final AuthenticatedLandlord authenticatedLandlord;
- public RentPaymentController(RentPaymentDraftService drafts, ConfirmRentPaymentService confirmation,
- AllocateRentPaymentService allocation, ReverseRentPaymentService reversal,
- RentPaymentRefundDraftService refundDrafts,
- ConfirmRentPaymentRefundService refundConfirmation,
+ public RentPaymentController(RentPaymentHistoryService history, RecordRentPaymentService recording,
+ AllocateRentPaymentService allocation, RefundRentPaymentService refunds,
+ ReverseRentPaymentService reversal,
GeneratePaymentReceiptService receipts,
AuthenticatedLandlord authenticatedLandlord) {
- this.drafts = drafts;
- this.confirmation = confirmation;
+ this.history = history;
+ this.recording = recording;
this.allocation = allocation;
+ this.refunds = refunds;
this.reversal = reversal;
- this.refundDrafts = refundDrafts;
- this.refundConfirmation = refundConfirmation;
this.receipts = receipts;
this.authenticatedLandlord = authenticatedLandlord;
}
@@ -67,67 +58,45 @@ public class RentPaymentController {
List list(@AuthenticationPrincipal OidcUser principal,
@RequestParam(required = false) String leaseId) {
LandlordId landlordId = authenticatedLandlord.id(principal);
- var payments = leaseId == null ? drafts.list(landlordId) : drafts.list(landlordId, new LeaseId(leaseId));
- return payments.stream().map(RentPaymentApi.Response::from).toList();
+ return history.list(landlordId, leaseId == null ? null : new LeaseId(leaseId)).stream()
+ .map(RentPaymentApi.Response::from).toList();
}
@GetMapping("/{paymentId}")
RentPaymentApi.Response get(@AuthenticationPrincipal OidcUser principal, @PathVariable String paymentId) {
- return RentPaymentApi.Response.from(drafts.get(authenticatedLandlord.id(principal),
+ return RentPaymentApi.Response.from(history.get(authenticatedLandlord.id(principal),
new RentPaymentId(paymentId)));
}
@PostMapping
ResponseEntity create(@AuthenticationPrincipal OidcUser principal,
@Valid @RequestBody RentPaymentApi.CreateRequest request) {
- var created = drafts.create(authenticatedLandlord.id(principal), new LeaseId(request.leaseId()),
- request.receivedAt(), request.amount());
- return ResponseEntity.status(HttpStatus.CREATED).body(RentPaymentApi.Response.from(created));
- }
-
- @PutMapping("/{paymentId}")
- RentPaymentApi.Response revise(@AuthenticationPrincipal OidcUser principal, @PathVariable String paymentId,
- @Valid @RequestBody RentPaymentApi.ReviseRequest request) {
- return RentPaymentApi.Response.from(drafts.revise(authenticatedLandlord.id(principal),
- new RentPaymentId(paymentId), request.receivedAt(), request.amount()));
- }
-
- @DeleteMapping("/{paymentId}")
- ResponseEntity delete(@AuthenticationPrincipal OidcUser principal, @PathVariable String paymentId) {
- drafts.delete(authenticatedLandlord.id(principal), new RentPaymentId(paymentId));
- return ResponseEntity.noContent().build();
- }
-
- @PostMapping("/{paymentId}/confirm")
- RentPaymentApi.Response confirm(@AuthenticationPrincipal OidcUser principal, @PathVariable String paymentId,
- @Valid @RequestBody RentPaymentApi.ConfirmRequest request) {
- var requested = request.allocations().stream().map(item -> new ConfirmRentPaymentService.RequestedAllocation(
- new RentDueId(item.rentDueId()), item.amount())).toList();
- return RentPaymentApi.Response.from(confirmation.confirm(authenticatedLandlord.id(principal),
- new RentPaymentId(paymentId), requested));
+ LandlordId landlordId = authenticatedLandlord.id(principal);
+ var created = recording.record(landlordId, request.operationId(), new RentDueId(request.rentDueId()),
+ request.rentDueVersion(), request.receivedAt(), request.amount());
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(RentPaymentApi.Response.from(history.get(landlordId, created.id())));
}
@PostMapping("/{paymentId}/allocations")
RentPaymentApi.Response allocate(@AuthenticationPrincipal OidcUser principal, @PathVariable String paymentId,
@Valid @RequestBody RentPaymentApi.AllocationRequest request) {
- return RentPaymentApi.Response.from(allocation.allocate(authenticatedLandlord.id(principal),
- new RentPaymentId(paymentId), new RentDueId(request.rentDueId()), request.amount()));
+ LandlordId landlordId = authenticatedLandlord.id(principal);
+ allocation.allocate(landlordId, new RentPaymentId(paymentId), request.operationId(),
+ request.paymentVersion(), new RentDueId(request.rentDueId()), request.rentDueVersion(),
+ request.amount());
+ return RentPaymentApi.Response.from(history.get(landlordId, new RentPaymentId(paymentId)));
}
@PostMapping("/{paymentId}/reversal")
- ResponseEntity reverse(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId,
- @Valid @RequestBody RentPaymentApi.ReversalRequest request) {
- var reversed = reversal.reverse(authenticatedLandlord.id(principal), new RentPaymentId(paymentId),
- request.reason());
- return ResponseEntity.status(HttpStatus.CREATED).body(RentPaymentApi.ReversalResponse.from(reversed));
- }
-
- @GetMapping("/{paymentId}/reversal")
- RentPaymentApi.ReversalResponse reversal(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId) {
- return RentPaymentApi.ReversalResponse.from(reversal.get(authenticatedLandlord.id(principal),
- new RentPaymentId(paymentId)));
+ ResponseEntity reverse(@AuthenticationPrincipal OidcUser principal,
+ @PathVariable String paymentId,
+ @Valid @RequestBody RentPaymentApi.ReversalRequest request) {
+ LandlordId landlordId = authenticatedLandlord.id(principal);
+ RentPaymentId id = new RentPaymentId(paymentId);
+ reversal.reverse(landlordId, id, request.paymentVersion(), request.reason());
+ return ResponseEntity.status(HttpStatus.CREATED).body(RentPaymentApi.Response.from(history.get(landlordId,
+ id)));
}
@GetMapping("/{paymentId}/receipt")
@@ -135,54 +104,16 @@ public class RentPaymentController {
return document(receipts.generate(authenticatedLandlord.id(principal), new RentPaymentId(paymentId)));
}
- @GetMapping("/{paymentId}/refunds")
- List refunds(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId) {
- return refundDrafts.list(authenticatedLandlord.id(principal), new RentPaymentId(paymentId)).stream()
- .map(RentPaymentApi.RefundResponse::from).toList();
- }
-
@PostMapping("/{paymentId}/refunds")
- ResponseEntity createRefund(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId,
- @Valid @RequestBody RentPaymentApi.RefundRequest request) {
- var created = refundDrafts.create(authenticatedLandlord.id(principal), new RentPaymentId(paymentId),
- request.amount(), request.refundedAt());
- return ResponseEntity.status(HttpStatus.CREATED).body(RentPaymentApi.RefundResponse.from(created));
- }
-
- @PutMapping("/{paymentId}/refunds/{refundId}")
- RentPaymentApi.RefundResponse reviseRefund(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId, @PathVariable String refundId,
- @Valid @RequestBody RentPaymentApi.RefundRequest request) {
+ ResponseEntity refund(@AuthenticationPrincipal OidcUser principal,
+ @PathVariable String paymentId,
+ @Valid @RequestBody RentPaymentApi.RefundRequest request) {
LandlordId landlordId = authenticatedLandlord.id(principal);
- requirePayment(refundDrafts.get(landlordId, new RentPaymentRefundId(refundId)), paymentId);
- return RentPaymentApi.RefundResponse.from(refundDrafts.revise(landlordId,
- new RentPaymentRefundId(refundId), request.amount(), request.refundedAt()));
- }
-
- @DeleteMapping("/{paymentId}/refunds/{refundId}")
- ResponseEntity deleteRefund(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId, @PathVariable String refundId) {
- LandlordId landlordId = authenticatedLandlord.id(principal);
- requirePayment(refundDrafts.get(landlordId, new RentPaymentRefundId(refundId)), paymentId);
- refundDrafts.delete(landlordId, new RentPaymentRefundId(refundId));
- return ResponseEntity.noContent().build();
- }
-
- @PostMapping("/{paymentId}/refunds/{refundId}/confirm")
- RentPaymentApi.RefundResponse confirmRefund(@AuthenticationPrincipal OidcUser principal,
- @PathVariable String paymentId, @PathVariable String refundId) {
- LandlordId landlordId = authenticatedLandlord.id(principal);
- requirePayment(refundDrafts.get(landlordId, new RentPaymentRefundId(refundId)), paymentId);
- return RentPaymentApi.RefundResponse.from(refundConfirmation.confirm(landlordId,
- new RentPaymentRefundId(refundId)));
- }
-
- private static void requirePayment(RentPaymentRefund refund, String paymentId) {
- if (!refund.paymentId().equals(new RentPaymentId(paymentId))) {
- throw new ResourceNotFound("Rent payment refund");
- }
+ RentPaymentId id = new RentPaymentId(paymentId);
+ refunds.refund(landlordId, id, request.operationId(), request.paymentVersion(), request.amount(),
+ request.refundedAt());
+ return ResponseEntity.status(HttpStatus.CREATED).body(RentPaymentApi.Response.from(history.get(landlordId,
+ id)));
}
private static ResponseEntity document(GeneratedDocument document) {
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRefundRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRefundRepository.java
index ea71c9a..9116075 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRefundRepository.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRefundRepository.java
@@ -7,7 +7,6 @@ import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundStatus;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -32,6 +31,12 @@ public class MongoRentPaymentRefundRepository implements RentPaymentRefundReposi
this.mongoTemplate = mongoTemplate;
}
+ @Override
+ public List findAll(LandlordId landlordId) {
+ return mongoTemplate.find(Query.query(Criteria.where("landlordId").is(landlordId.value())),
+ RefundDocument.class).stream().map(this::toDomain).toList();
+ }
+
@Override
public List findAll(LandlordId landlordId, RentPaymentId paymentId) {
Query query = Query.query(Criteria.where("landlordId").is(landlordId.value())
@@ -52,36 +57,20 @@ public class MongoRentPaymentRefundRepository implements RentPaymentRefundReposi
refund.version(), document::withVersion));
}
- @Override
- public boolean deleteDraft(RentPaymentRefund refund) {
- Query query = scoped(refund.landlordId(), refund.id());
- query.addCriteria(Criteria.where("status").is(RentPaymentRefundStatus.DRAFT.name()));
- query.addCriteria(versionCriteria(refund.version()));
- return mongoTemplate.remove(query, RefundDocument.class).getDeletedCount() == 1;
- }
-
private static Query scoped(LandlordId landlordId, RentPaymentRefundId refundId) {
return Query.query(Criteria.where("_id").is(refundId.value())
.and("landlordId").is(landlordId.value()));
}
- private static Criteria versionCriteria(long version) {
- return version < 0
- ? new Criteria().orOperator(Criteria.where("version").exists(false), Criteria.where("version").is(null))
- : Criteria.where("version").is(version);
- }
-
private RefundDocument toDocument(RentPaymentRefund refund) {
return new RefundDocument(refund.id().value(), refund.landlordId().value(), refund.leaseId().value(),
- refund.paymentId().value(), refund.amount(), refund.refundedAt(), refund.status().name(),
- refund.confirmedAt(), persistedVersion(refund.version()));
+ refund.paymentId().value(), refund.amount(), refund.refundedAt(), persistedVersion(refund.version()));
}
private RentPaymentRefund toDomain(RefundDocument document) {
return RentPaymentRefund.reconstitute(new RentPaymentRefundId(document.id()),
new LandlordId(document.landlordId()), new LeaseId(document.leaseId()),
new RentPaymentId(document.paymentId()), document.amount(), document.refundedAt(),
- RentPaymentRefundStatus.valueOf(document.status()), document.confirmedAt(),
domainVersion(document.version()));
}
@@ -96,11 +85,9 @@ public class MongoRentPaymentRefundRepository implements RentPaymentRefundReposi
@Document(collection = "rentPaymentRefunds")
record RefundDocument(@Id String id, @Indexed String landlordId, @Indexed String leaseId,
@Indexed String paymentId, @Field(targetType = FieldType.DECIMAL128) BigDecimal amount,
- LocalDate refundedAt, String status,
- LocalDate confirmedAt, @Version Long version) {
+ LocalDate refundedAt, @Version Long version) {
RefundDocument withVersion(Long version) {
- return new RefundDocument(id, landlordId, leaseId, paymentId, amount, refundedAt, status, confirmedAt,
- version);
+ return new RefundDocument(id, landlordId, leaseId, paymentId, amount, refundedAt, version);
}
}
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRepository.java
index 57b8a16..09dd23d 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRepository.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/adapter/out/mongodb/rent/MongoRentPaymentRepository.java
@@ -58,14 +58,6 @@ public class MongoRentPaymentRepository implements RentPaymentRepository {
payment.version(), document::withVersion));
}
- @Override
- public boolean deleteDraft(RentPayment payment) {
- Query query = scoped(payment.landlordId(), payment.id());
- query.addCriteria(Criteria.where("status").is(RentPaymentStatus.DRAFT.name()));
- query.addCriteria(versionCriteria(payment.version()));
- return mongoTemplate.remove(query, RentPaymentDocument.class).getDeletedCount() == 1;
- }
-
private List find(Query query) {
return mongoTemplate.find(query, RentPaymentDocument.class).stream().map(this::toDomain).toList();
}
@@ -75,12 +67,6 @@ public class MongoRentPaymentRepository implements RentPaymentRepository {
.and("landlordId").is(landlordId.value()));
}
- private static Criteria versionCriteria(long version) {
- return version < 0
- ? new Criteria().orOperator(Criteria.where("version").exists(false), Criteria.where("version").is(null))
- : Criteria.where("version").is(version);
- }
-
private RentPaymentDocument toDocument(RentPayment payment) {
return new RentPaymentDocument(payment.id().value(), payment.landlordId().value(), payment.leaseId().value(),
payment.receivedAt(), payment.amount(), payment.status().name(),
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/port/out/RentalDocumentRenderer.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/port/out/RentalDocumentRenderer.java
index 0be059b..622605c 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/port/out/RentalDocumentRenderer.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/port/out/RentalDocumentRenderer.java
@@ -46,8 +46,7 @@ public interface RentalDocumentRenderer {
BigDecimal amount, LocalDate allocatedAt) {
}
- record ReceiptRefund(RentPaymentRefundId refundId, BigDecimal amount, LocalDate refundedAt,
- LocalDate confirmedAt) {
+ record ReceiptRefund(RentPaymentRefundId refundId, BigDecimal amount, LocalDate refundedAt) {
}
record RentReceipt(RentDueId rentDueId, YearMonth month, BigDecimal rent, BigDecimal charges,
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/AllocateRentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/AllocateRentPaymentService.java
index 39face0..9179475 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/AllocateRentPaymentService.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/AllocateRentPaymentService.java
@@ -1,7 +1,7 @@
package fr.nessar.lmnporganizer.rental.application.rent;
-import fr.nessar.lmnporganizer.rental.application.port.out.IdentifierGenerator;
import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
+import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocation;
@@ -9,6 +9,7 @@ import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocationId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentDue;
import fr.nessar.lmnporganizer.rental.domain.rent.RentDueId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentDueRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentDueSettlement;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
@@ -22,30 +23,48 @@ public final class AllocateRentPaymentService {
private final RentPaymentRepository payments;
private final RentDueRepository rentDues;
private final TransactionRunner transactions;
- private final IdentifierGenerator identifiers;
private final Clock clock;
public AllocateRentPaymentService(RentPaymentRepository payments, RentDueRepository rentDues,
- TransactionRunner transactions, IdentifierGenerator identifiers, Clock clock) {
+ TransactionRunner transactions, Clock clock) {
this.payments = payments;
this.rentDues = rentDues;
this.transactions = transactions;
- this.identifiers = identifiers;
this.clock = clock;
}
- public RentPayment allocate(LandlordId landlordId, RentPaymentId paymentId, RentDueId rentDueId,
- BigDecimal amount) {
+ public RentPayment allocate(LandlordId landlordId, RentPaymentId paymentId, String operationId,
+ long paymentVersion, RentDueId rentDueId, long rentDueVersion, BigDecimal amount) {
return transactions.run(() -> {
RentPayment payment = payments.find(landlordId, paymentId)
.orElseThrow(() -> new ResourceNotFound("Rent payment"));
+ PaymentAllocationId allocationId = new PaymentAllocationId(operationId);
+ PaymentAllocation existing = payment.allocations().stream()
+ .filter(allocation -> allocation.id().equals(allocationId)).findFirst().orElse(null);
+ if (existing != null) {
+ if (!existing.rentDueId().equals(rentDueId) || amount == null
+ || existing.amount().compareTo(amount) != 0) {
+ throw new RentPaymentConflict(
+ "Cet identifiant d'opération a déjà été utilisé avec d'autres données.");
+ }
+ return payment;
+ }
+ if (payment.version() != paymentVersion) {
+ throw new ConcurrentChange("Rent payment");
+ }
RentDue due = rentDues.find(landlordId, rentDueId)
.orElseThrow(() -> new ResourceNotFound("Rent due"));
+ if (due.version() != rentDueVersion) {
+ throw new ConcurrentChange("Rent due");
+ }
+ if (due.settlement() == RentDueSettlement.PAID) {
+ throw new RentPaymentConflict("Une échéance déjà réglée ne peut pas recevoir une affectation.");
+ }
if (!payment.leaseId().equals(due.leaseId())) {
throw new RentRuleViolation("Une affectation doit viser une échéance du même bail.");
}
- PaymentAllocation allocation = new PaymentAllocation(new PaymentAllocationId(identifiers.next()),
- due.id(), amount, LocalDate.now(clock));
+ PaymentAllocation allocation = new PaymentAllocation(allocationId, due.id(), amount,
+ LocalDate.now(clock));
payment.allocate(allocation);
due.allocate(amount);
rentDues.save(due);
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentRefundService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentRefundService.java
deleted file mode 100644
index fdabe5d..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentRefundService.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package fr.nessar.lmnporganizer.rental.application.rent;
-
-import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
-import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
-
-import java.time.Clock;
-import java.time.LocalDate;
-
-public final class ConfirmRentPaymentRefundService {
- private final RentPaymentRepository payments;
- private final RentPaymentRefundRepository refunds;
- private final TransactionRunner transactions;
- private final Clock clock;
-
- public ConfirmRentPaymentRefundService(RentPaymentRepository payments, RentPaymentRefundRepository refunds,
- TransactionRunner transactions, Clock clock) {
- this.payments = payments;
- this.refunds = refunds;
- this.transactions = transactions;
- this.clock = clock;
- }
-
- public RentPaymentRefund confirm(LandlordId landlordId, RentPaymentRefundId refundId) {
- return transactions.run(() -> {
- RentPaymentRefund refund = refunds.find(landlordId, refundId)
- .orElseThrow(() -> new ResourceNotFound("Rent payment refund"));
- RentPayment payment = payments.find(landlordId, refund.paymentId())
- .orElseThrow(() -> new ResourceNotFound("Rent payment"));
- refund.confirm(payment, LocalDate.now(clock));
- payments.save(payment);
- return refunds.save(refund);
- });
- }
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentService.java
deleted file mode 100644
index 75273e0..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ConfirmRentPaymentService.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package fr.nessar.lmnporganizer.rental.application.rent;
-
-import fr.nessar.lmnporganizer.rental.application.port.out.IdentifierGenerator;
-import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
-import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocation;
-import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocationId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentDue;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentDueId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentDueRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentRuleViolation;
-
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-
-public final class ConfirmRentPaymentService {
- private final RentPaymentRepository payments;
- private final RentDueRepository rentDues;
- private final TransactionRunner transactions;
- private final IdentifierGenerator identifiers;
- private final Clock clock;
-
- public ConfirmRentPaymentService(RentPaymentRepository payments, RentDueRepository rentDues,
- TransactionRunner transactions, IdentifierGenerator identifiers, Clock clock) {
- this.payments = payments;
- this.rentDues = rentDues;
- this.transactions = transactions;
- this.identifiers = identifiers;
- this.clock = clock;
- }
-
- public RentPayment confirm(LandlordId landlordId, RentPaymentId paymentId, List requested) {
- return transactions.run(() -> confirmInTransaction(landlordId, paymentId, requested));
- }
-
- private RentPayment confirmInTransaction(LandlordId landlordId, RentPaymentId paymentId,
- List requested) {
- RentPayment payment = payments.find(landlordId, paymentId)
- .orElseThrow(() -> new ResourceNotFound("Rent payment"));
- LocalDate confirmationDate = LocalDate.now(clock);
- List allocationsToApply = requested == null ? List.of() : List.copyOf(requested);
- if (new HashSet<>(allocationsToApply.stream().map(RequestedAllocation::rentDueId).toList()).size()
- != allocationsToApply.size()) {
- throw new RentRuleViolation("Un règlement ne peut avoir qu'une affectation par échéance.");
- }
- List affectedDues = new ArrayList<>();
- List allocations = new ArrayList<>();
- for (RequestedAllocation allocation : allocationsToApply) {
- RentDue due = rentDues.find(landlordId, allocation.rentDueId())
- .orElseThrow(() -> new ResourceNotFound("Rent due"));
- if (!payment.leaseId().equals(due.leaseId())) {
- throw new RentRuleViolation("Une affectation doit viser une échéance du même bail.");
- }
- due.allocate(allocation.amount());
- affectedDues.add(due);
- allocations.add(new PaymentAllocation(new PaymentAllocationId(identifiers.next()), due.id(),
- allocation.amount(), confirmationDate));
- }
- payment.confirm(allocations, confirmationDate);
- rentDues.saveAll(affectedDues);
- return payments.save(payment);
- }
-
- public record RequestedAllocation(RentDueId rentDueId, BigDecimal amount) {
- }
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/GeneratePaymentReceiptService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/GeneratePaymentReceiptService.java
index 7978f33..e6e1539 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/GeneratePaymentReceiptService.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/GeneratePaymentReceiptService.java
@@ -17,7 +17,6 @@ import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundStatus;
import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentStatus;
import fr.nessar.lmnporganizer.rental.domain.rent.RentRuleViolation;
@@ -50,9 +49,7 @@ public final class GeneratePaymentReceiptService {
payment.amount(), payment.refundedAmount(), payment.unallocatedBalance(), contract,
payment.allocations().stream().map(allocation -> allocation(allocation, landlordId)).toList(),
refunds.findAll(landlordId, paymentId).stream()
- .filter(refund -> refund.status() == RentPaymentRefundStatus.CONFIRMED)
- .map(refund -> new ReceiptRefund(refund.id(), refund.amount(), refund.refundedAt(),
- refund.confirmedAt()))
+ .map(refund -> new ReceiptRefund(refund.id(), refund.amount(), refund.refundedAt()))
.toList());
return new GeneratedDocument("recu-reglement-" + payment.id().value() + ".pdf", "application/pdf",
renderer.renderPaymentReceipt(receipt));
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RecordRentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RecordRentPaymentService.java
new file mode 100644
index 0000000..59e1215
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RecordRentPaymentService.java
@@ -0,0 +1,85 @@
+package fr.nessar.lmnporganizer.rental.application.rent;
+
+import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
+import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
+import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
+import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
+import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocation;
+import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocationId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentDue;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentDueId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentDueRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentDueSettlement;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentRuleViolation;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.List;
+
+public final class RecordRentPaymentService {
+ private final RentPaymentRepository payments;
+ private final RentDueRepository rentDues;
+ private final TransactionRunner transactions;
+ private final Clock clock;
+
+ public RecordRentPaymentService(RentPaymentRepository payments, RentDueRepository rentDues,
+ TransactionRunner transactions, Clock clock) {
+ this.payments = payments;
+ this.rentDues = rentDues;
+ this.transactions = transactions;
+ this.clock = clock;
+ }
+
+ public RentPayment record(LandlordId landlordId, String operationId, RentDueId rentDueId, long rentDueVersion,
+ LocalDate receivedAt, BigDecimal amount) {
+ RentPaymentId paymentId = new RentPaymentId(operationId);
+ return transactions.run(() -> payments.find(landlordId, paymentId)
+ .map(existing -> idempotentPayment(existing, rentDueId, receivedAt, amount))
+ .orElseGet(() -> recordInTransaction(landlordId, paymentId, rentDueId, rentDueVersion, receivedAt,
+ amount)));
+ }
+
+ private RentPayment idempotentPayment(RentPayment existing, RentDueId rentDueId, LocalDate receivedAt,
+ BigDecimal amount) {
+ boolean sameInitialDue = existing.allocations().stream()
+ .filter(allocation -> allocation.id().value().equals(existing.id().value()))
+ .anyMatch(allocation -> allocation.rentDueId().equals(rentDueId));
+ if (!existing.receivedAt().equals(receivedAt) || !sameAmount(existing.amount(), amount) || !sameInitialDue) {
+ throw new RentPaymentConflict("Cet identifiant d'opération a déjà été utilisé avec d'autres données.");
+ }
+ return existing;
+ }
+
+ private RentPayment recordInTransaction(LandlordId landlordId, RentPaymentId paymentId, RentDueId rentDueId,
+ long rentDueVersion, LocalDate receivedAt, BigDecimal amount) {
+ LocalDate today = LocalDate.now(clock);
+ if (receivedAt == null || receivedAt.isAfter(today)) {
+ throw new RentRuleViolation("La date de réception du règlement ne peut pas être future.");
+ }
+ RentDue due = rentDues.find(landlordId, rentDueId).orElseThrow(() -> new ResourceNotFound("Rent due"));
+ if (due.version() != rentDueVersion) {
+ throw new ConcurrentChange("Rent due");
+ }
+ if (due.settlement() == RentDueSettlement.PAID) {
+ throw new RentPaymentConflict("Une échéance déjà réglée ne peut pas recevoir un nouveau règlement.");
+ }
+
+ RentPayment payment = RentPayment.confirmed(paymentId, landlordId, due.leaseId(), receivedAt, amount,
+ List.of(), today);
+ BigDecimal allocationAmount = payment.unallocatedBalance().min(due.remainingAmount());
+ PaymentAllocation allocation = new PaymentAllocation(new PaymentAllocationId(paymentId.value()), due.id(),
+ allocationAmount, today);
+ payment.allocate(allocation);
+ due.allocate(allocationAmount);
+ rentDues.save(due);
+ return payments.save(payment);
+ }
+
+ private static boolean sameAmount(BigDecimal first, BigDecimal second) {
+ return second != null && first.compareTo(second) == 0;
+ }
+}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RefundRentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RefundRentPaymentService.java
new file mode 100644
index 0000000..25097a7
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RefundRentPaymentService.java
@@ -0,0 +1,71 @@
+package fr.nessar.lmnporganizer.rental.application.rent;
+
+import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
+import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
+import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
+import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentRuleViolation;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDate;
+
+public final class RefundRentPaymentService {
+ private final RentPaymentRepository payments;
+ private final RentPaymentRefundRepository refunds;
+ private final TransactionRunner transactions;
+ private final Clock clock;
+
+ public RefundRentPaymentService(RentPaymentRepository payments, RentPaymentRefundRepository refunds,
+ TransactionRunner transactions, Clock clock) {
+ this.payments = payments;
+ this.refunds = refunds;
+ this.transactions = transactions;
+ this.clock = clock;
+ }
+
+ public RentPayment refund(LandlordId landlordId, RentPaymentId paymentId, String operationId,
+ long paymentVersion, BigDecimal amount, LocalDate refundedAt) {
+ RentPaymentRefundId refundId = new RentPaymentRefundId(operationId);
+ return transactions.run(() -> refunds.find(landlordId, refundId)
+ .map(existing -> idempotentPayment(landlordId, paymentId, amount, refundedAt, existing))
+ .orElseGet(() -> refundInTransaction(landlordId, paymentId, refundId, paymentVersion, amount,
+ refundedAt)));
+ }
+
+ private RentPayment refundInTransaction(LandlordId landlordId, RentPaymentId paymentId,
+ RentPaymentRefundId refundId, long paymentVersion, BigDecimal amount,
+ LocalDate refundedAt) {
+ LocalDate today = LocalDate.now(clock);
+ if (refundedAt == null || refundedAt.isAfter(today)) {
+ throw new RentRuleViolation("La date du remboursement ne peut pas être future.");
+ }
+ RentPayment payment = payment(landlordId, paymentId);
+ if (payment.version() != paymentVersion) {
+ throw new ConcurrentChange("Rent payment");
+ }
+ RentPaymentRefund refund = RentPaymentRefund.recorded(refundId, payment, amount, refundedAt);
+ RentPayment saved = payments.save(payment);
+ refunds.save(refund);
+ return saved;
+ }
+
+ private RentPayment idempotentPayment(LandlordId landlordId, RentPaymentId paymentId, BigDecimal amount,
+ LocalDate refundedAt, RentPaymentRefund existing) {
+ if (!existing.paymentId().equals(paymentId) || !existing.refundedAt().equals(refundedAt)
+ || amount == null || existing.amount().compareTo(amount) != 0) {
+ throw new RentPaymentConflict("Cet identifiant d'opération a déjà été utilisé avec d'autres données.");
+ }
+ return payment(landlordId, paymentId);
+ }
+
+ private RentPayment payment(LandlordId landlordId, RentPaymentId paymentId) {
+ return payments.find(landlordId, paymentId).orElseThrow(() -> new ResourceNotFound("Rent payment"));
+ }
+}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentConflict.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentConflict.java
new file mode 100644
index 0000000..c9d68c4
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentConflict.java
@@ -0,0 +1,7 @@
+package fr.nessar.lmnporganizer.rental.application.rent;
+
+public final class RentPaymentConflict extends RuntimeException {
+ public RentPaymentConflict(String message) {
+ super(message);
+ }
+}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentDraftService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentDraftService.java
deleted file mode 100644
index bb9e0cb..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentDraftService.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package fr.nessar.lmnporganizer.rental.application.rent;
-
-import fr.nessar.lmnporganizer.rental.application.port.out.IdentifierGenerator;
-import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
-import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.lease.Lease;
-import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
-import fr.nessar.lmnporganizer.rental.domain.lease.LeaseRepository;
-import fr.nessar.lmnporganizer.rental.domain.lease.LeaseStatus;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentRuleViolation;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.util.List;
-
-public final class RentPaymentDraftService {
- private final LeaseRepository leases;
- private final RentPaymentRepository payments;
- private final IdentifierGenerator identifiers;
-
- public RentPaymentDraftService(LeaseRepository leases, RentPaymentRepository payments,
- IdentifierGenerator identifiers) {
- this.leases = leases;
- this.payments = payments;
- this.identifiers = identifiers;
- }
-
- public List list(LandlordId landlordId) {
- return payments.findAll(landlordId);
- }
-
- public List list(LandlordId landlordId, LeaseId leaseId) {
- lease(landlordId, leaseId);
- return payments.findAll(landlordId, leaseId);
- }
-
- public RentPayment get(LandlordId landlordId, RentPaymentId paymentId) {
- return payments.find(landlordId, paymentId).orElseThrow(() -> new ResourceNotFound("Rent payment"));
- }
-
- public RentPayment create(LandlordId landlordId, LeaseId leaseId, LocalDate receivedAt, BigDecimal amount) {
- Lease lease = lease(landlordId, leaseId);
- if (lease.status() == LeaseStatus.DRAFT) {
- throw new RentRuleViolation("Un règlement ne peut pas être enregistré pour un bail en brouillon.");
- }
- return payments.save(RentPayment.draft(new RentPaymentId(identifiers.next()), landlordId, leaseId,
- receivedAt, amount));
- }
-
- public RentPayment revise(LandlordId landlordId, RentPaymentId paymentId, LocalDate receivedAt,
- BigDecimal amount) {
- RentPayment payment = get(landlordId, paymentId);
- payment.revise(receivedAt, amount);
- return payments.save(payment);
- }
-
- public void delete(LandlordId landlordId, RentPaymentId paymentId) {
- RentPayment payment = get(landlordId, paymentId);
- if (!payments.deleteDraft(payment)) {
- throw new ConcurrentChange("Rent payment");
- }
- }
-
- private Lease lease(LandlordId landlordId, LeaseId leaseId) {
- return leases.find(landlordId, leaseId).orElseThrow(() -> new ResourceNotFound("Lease"));
- }
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistory.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistory.java
new file mode 100644
index 0000000..d8f6f80
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistory.java
@@ -0,0 +1,18 @@
+package fr.nessar.lmnporganizer.rental.application.rent;
+
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentReversal;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public record RentPaymentHistory(RentPayment payment, List refunds,
+ Optional reversal) {
+ public RentPaymentHistory {
+ Objects.requireNonNull(payment);
+ refunds = List.copyOf(refunds);
+ reversal = Objects.requireNonNull(reversal);
+ }
+}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistoryService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistoryService.java
new file mode 100644
index 0000000..ae1fbc8
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentHistoryService.java
@@ -0,0 +1,57 @@
+package fr.nessar.lmnporganizer.rental.application.rent;
+
+import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
+import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
+import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
+import fr.nessar.lmnporganizer.rental.domain.lease.LeaseRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentReversal;
+import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentReversalRepository;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public final class RentPaymentHistoryService {
+ private final RentPaymentRepository payments;
+ private final RentPaymentRefundRepository refunds;
+ private final RentPaymentReversalRepository reversals;
+ private final LeaseRepository leases;
+
+ public RentPaymentHistoryService(RentPaymentRepository payments, RentPaymentRefundRepository refunds,
+ RentPaymentReversalRepository reversals, LeaseRepository leases) {
+ this.payments = payments;
+ this.refunds = refunds;
+ this.reversals = reversals;
+ this.leases = leases;
+ }
+
+ public RentPaymentHistory get(LandlordId landlordId, RentPaymentId paymentId) {
+ RentPayment payment = payments.find(landlordId, paymentId)
+ .orElseThrow(() -> new ResourceNotFound("Rent payment"));
+ return new RentPaymentHistory(payment, refunds.findAll(landlordId, paymentId),
+ reversals.findByPayment(landlordId, paymentId));
+ }
+
+ public List list(LandlordId landlordId, LeaseId leaseId) {
+ List landlordPayments;
+ if (leaseId == null) {
+ landlordPayments = payments.findAll(landlordId);
+ } else {
+ leases.find(landlordId, leaseId).orElseThrow(() -> new ResourceNotFound("Lease"));
+ landlordPayments = payments.findAll(landlordId, leaseId);
+ }
+ Map> refundsByPayment = refunds.findAll(landlordId).stream()
+ .collect(Collectors.groupingBy(RentPaymentRefund::paymentId));
+ Map reversalsByPayment = reversals.findAll(landlordId).stream()
+ .collect(Collectors.toMap(RentPaymentReversal::paymentId, reversal -> reversal));
+ return landlordPayments.stream().map(payment -> new RentPaymentHistory(payment,
+ refundsByPayment.getOrDefault(payment.id(), List.of()),
+ Optional.ofNullable(reversalsByPayment.get(payment.id())))).toList();
+ }
+}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentRefundDraftService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentRefundDraftService.java
deleted file mode 100644
index d8a33c7..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/RentPaymentRefundDraftService.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package fr.nessar.lmnporganizer.rental.application.rent;
-
-import fr.nessar.lmnporganizer.rental.application.port.out.IdentifierGenerator;
-import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
-import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPayment;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefund;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundId;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRefundRepository;
-import fr.nessar.lmnporganizer.rental.domain.rent.RentPaymentRepository;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.util.List;
-
-public final class RentPaymentRefundDraftService {
- private final RentPaymentRepository payments;
- private final RentPaymentRefundRepository refunds;
- private final IdentifierGenerator identifiers;
-
- public RentPaymentRefundDraftService(RentPaymentRepository payments, RentPaymentRefundRepository refunds,
- IdentifierGenerator identifiers) {
- this.payments = payments;
- this.refunds = refunds;
- this.identifiers = identifiers;
- }
-
- public List list(LandlordId landlordId, RentPaymentId paymentId) {
- payment(landlordId, paymentId);
- return refunds.findAll(landlordId, paymentId);
- }
-
- public RentPaymentRefund get(LandlordId landlordId, RentPaymentRefundId refundId) {
- return refunds.find(landlordId, refundId)
- .orElseThrow(() -> new ResourceNotFound("Rent payment refund"));
- }
-
- public RentPaymentRefund create(LandlordId landlordId, RentPaymentId paymentId, BigDecimal amount,
- LocalDate refundedAt) {
- RentPayment payment = payment(landlordId, paymentId);
- payment.ensureRefundable(amount);
- return refunds.save(RentPaymentRefund.draft(new RentPaymentRefundId(identifiers.next()), landlordId,
- payment.leaseId(), paymentId, amount, refundedAt));
- }
-
- public RentPaymentRefund revise(LandlordId landlordId, RentPaymentRefundId refundId, BigDecimal amount,
- LocalDate refundedAt) {
- RentPaymentRefund refund = get(landlordId, refundId);
- payment(landlordId, refund.paymentId()).ensureRefundable(amount);
- refund.revise(amount, refundedAt);
- return refunds.save(refund);
- }
-
- public void delete(LandlordId landlordId, RentPaymentRefundId refundId) {
- RentPaymentRefund refund = get(landlordId, refundId);
- if (!refunds.deleteDraft(refund)) {
- throw new ConcurrentChange("Rent payment refund");
- }
- }
-
- private RentPayment payment(LandlordId landlordId, RentPaymentId paymentId) {
- return payments.find(landlordId, paymentId).orElseThrow(() -> new ResourceNotFound("Rent payment"));
- }
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ReverseRentPaymentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ReverseRentPaymentService.java
index 31563c0..26dd3ff 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ReverseRentPaymentService.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/application/rent/ReverseRentPaymentService.java
@@ -2,6 +2,7 @@ package fr.nessar.lmnporganizer.rental.application.rent;
import fr.nessar.lmnporganizer.rental.application.port.out.IdentifierGenerator;
import fr.nessar.lmnporganizer.rental.application.port.out.TransactionRunner;
+import fr.nessar.lmnporganizer.rental.application.shared.ConcurrentChange;
import fr.nessar.lmnporganizer.rental.application.shared.ResourceNotFound;
import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
import fr.nessar.lmnporganizer.rental.domain.rent.PaymentAllocation;
@@ -38,20 +39,20 @@ public final class ReverseRentPaymentService {
this.clock = clock;
}
- public RentPaymentReversal reverse(LandlordId landlordId, RentPaymentId paymentId, String reason) {
+ public RentPaymentReversal reverse(LandlordId landlordId, RentPaymentId paymentId, long paymentVersion,
+ String reason) {
return transactions.run(() -> reversals.findByPayment(landlordId, paymentId)
- .orElseGet(() -> reverseInTransaction(landlordId, paymentId, reason)));
- }
-
- public RentPaymentReversal get(LandlordId landlordId, RentPaymentId paymentId) {
- return reversals.findByPayment(landlordId, paymentId)
- .orElseThrow(() -> new ResourceNotFound("Rent payment reversal"));
+ .map(existing -> idempotentReversal(existing, reason))
+ .orElseGet(() -> reverseInTransaction(landlordId, paymentId, paymentVersion, reason)));
}
private RentPaymentReversal reverseInTransaction(LandlordId landlordId, RentPaymentId paymentId,
- String reason) {
+ long paymentVersion, String reason) {
RentPayment payment = payments.find(landlordId, paymentId)
.orElseThrow(() -> new ResourceNotFound("Rent payment"));
+ if (payment.version() != paymentVersion) {
+ throw new ConcurrentChange("Rent payment");
+ }
RentPaymentReversal reversal = RentPaymentReversal.reverse(
new RentPaymentReversalId(identifiers.next()), payment, LocalDate.now(clock), reason);
List affectedDues = new ArrayList<>();
@@ -65,4 +66,11 @@ public final class ReverseRentPaymentService {
payments.save(payment);
return reversals.save(reversal);
}
+
+ private RentPaymentReversal idempotentReversal(RentPaymentReversal existing, String reason) {
+ if (reason == null || !existing.reason().equals(reason.trim())) {
+ throw new RentPaymentConflict("Ce règlement a déjà été contrepassé pour une autre raison.");
+ }
+ return existing;
+ }
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPayment.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPayment.java
index 9b431e4..0724bab 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPayment.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPayment.java
@@ -14,12 +14,12 @@ public final class RentPayment {
private final RentPaymentId id;
private final LandlordId landlordId;
private final LeaseId leaseId;
- private LocalDate receivedAt;
- private BigDecimal amount;
+ private final LocalDate receivedAt;
+ private final BigDecimal amount;
private RentPaymentStatus status;
private List allocations;
private BigDecimal refundedAmount;
- private LocalDate confirmedAt;
+ private final LocalDate confirmedAt;
private final long version;
private RentPayment(RentPaymentId id, LandlordId landlordId, LeaseId leaseId, LocalDate receivedAt,
@@ -41,10 +41,11 @@ public final class RentPayment {
validateState();
}
- public static RentPayment draft(RentPaymentId id, LandlordId landlordId, LeaseId leaseId, LocalDate receivedAt,
- BigDecimal amount) {
- return new RentPayment(id, landlordId, leaseId, receivedAt, amount, RentPaymentStatus.DRAFT, List.of(),
- BigDecimal.ZERO, null, -1);
+ public static RentPayment confirmed(RentPaymentId id, LandlordId landlordId, LeaseId leaseId,
+ LocalDate receivedAt, BigDecimal amount,
+ List allocations, LocalDate confirmedAt) {
+ return new RentPayment(id, landlordId, leaseId, receivedAt, amount, RentPaymentStatus.CONFIRMED,
+ allocations, BigDecimal.ZERO, confirmedAt, -1);
}
public static RentPayment reconstitute(RentPaymentId id, LandlordId landlordId, LeaseId leaseId,
@@ -55,22 +56,6 @@ public final class RentPayment {
confirmedAt, version);
}
- public void revise(LocalDate receivedAt, BigDecimal amount) {
- requireDraft();
- this.receivedAt = Objects.requireNonNull(receivedAt);
- this.amount = positive(amount, "Le montant du règlement doit être strictement positif.");
- }
-
- public RentPaymentConfirmed confirm(List initialAllocations, LocalDate confirmationDate) {
- requireDraft();
- List requested = initialAllocations == null ? List.of() : List.copyOf(initialAllocations);
- validateAllocations(requested);
- allocations = requested;
- confirmedAt = Objects.requireNonNull(confirmationDate);
- status = RentPaymentStatus.CONFIRMED;
- return new RentPaymentConfirmed(id, landlordId, leaseId, amount, receivedAt, confirmedAt, allocations);
- }
-
public void allocate(PaymentAllocation allocation) {
requireConfirmed();
Objects.requireNonNull(allocation);
@@ -112,11 +97,7 @@ public final class RentPayment {
private void validateState() {
validateAllocations(allocations);
- if (status == RentPaymentStatus.DRAFT
- && (!allocations.isEmpty() || refundedAmount.signum() != 0 || confirmedAt != null)) {
- throw new RentRuleViolation("Un règlement en brouillon ne peut pas avoir d'écritures confirmées.");
- }
- if (status != RentPaymentStatus.DRAFT && confirmedAt == null) {
+ if (confirmedAt == null) {
throw new RentRuleViolation("Un règlement confirmé doit conserver sa date de confirmation.");
}
if (unallocatedBalance().signum() < 0) {
@@ -138,12 +119,6 @@ public final class RentPayment {
}
}
- private void requireDraft() {
- if (status != RentPaymentStatus.DRAFT) {
- throw new RentRuleViolation("Seul un règlement en brouillon peut être modifié.");
- }
- }
-
private void requireConfirmed() {
if (status != RentPaymentStatus.CONFIRMED) {
throw new RentRuleViolation("Le règlement doit être confirmé et non contrepassé.");
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentConfirmed.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentConfirmed.java
deleted file mode 100644
index f2604a1..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentConfirmed.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package fr.nessar.lmnporganizer.rental.domain.rent;
-
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.util.List;
-
-public record RentPaymentConfirmed(RentPaymentId paymentId, LandlordId landlordId, LeaseId leaseId,
- BigDecimal amount, LocalDate receivedAt, LocalDate confirmedAt,
- List allocations) {
- public RentPaymentConfirmed {
- allocations = List.copyOf(allocations);
- }
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefund.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefund.java
index d1a05b1..7e4a9f1 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefund.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefund.java
@@ -12,70 +12,37 @@ public final class RentPaymentRefund {
private final LandlordId landlordId;
private final LeaseId leaseId;
private final RentPaymentId paymentId;
- private BigDecimal amount;
- private LocalDate refundedAt;
- private RentPaymentRefundStatus status;
- private LocalDate confirmedAt;
+ private final BigDecimal amount;
+ private final LocalDate refundedAt;
private final long version;
private RentPaymentRefund(RentPaymentRefundId id, LandlordId landlordId, LeaseId leaseId,
RentPaymentId paymentId, BigDecimal amount, LocalDate refundedAt,
- RentPaymentRefundStatus status, LocalDate confirmedAt, long version) {
+ long version) {
this.id = Objects.requireNonNull(id);
this.landlordId = Objects.requireNonNull(landlordId);
this.leaseId = Objects.requireNonNull(leaseId);
this.paymentId = Objects.requireNonNull(paymentId);
this.amount = positive(amount);
this.refundedAt = Objects.requireNonNull(refundedAt);
- this.status = Objects.requireNonNull(status);
- this.confirmedAt = confirmedAt;
if (version < -1) {
throw new IllegalArgumentException("Rent payment refund version cannot be less than -1.");
}
this.version = version;
- if ((status == RentPaymentRefundStatus.DRAFT && confirmedAt != null)
- || (status == RentPaymentRefundStatus.CONFIRMED && confirmedAt == null)) {
- throw new RentRuleViolation("Le statut du remboursement ne correspond pas à sa confirmation.");
- }
}
- public static RentPaymentRefund draft(RentPaymentRefundId id, LandlordId landlordId, LeaseId leaseId,
- RentPaymentId paymentId, BigDecimal amount, LocalDate refundedAt) {
- return new RentPaymentRefund(id, landlordId, leaseId, paymentId, amount, refundedAt,
- RentPaymentRefundStatus.DRAFT, null, -1);
+ public static RentPaymentRefund recorded(RentPaymentRefundId id, RentPayment payment, BigDecimal amount,
+ LocalDate refundedAt) {
+ Objects.requireNonNull(payment);
+ payment.registerRefund(amount);
+ return new RentPaymentRefund(id, payment.landlordId(), payment.leaseId(), payment.id(), amount, refundedAt,
+ -1);
}
public static RentPaymentRefund reconstitute(RentPaymentRefundId id, LandlordId landlordId, LeaseId leaseId,
RentPaymentId paymentId, BigDecimal amount, LocalDate refundedAt,
- RentPaymentRefundStatus status, LocalDate confirmedAt,
long version) {
- return new RentPaymentRefund(id, landlordId, leaseId, paymentId, amount, refundedAt, status, confirmedAt,
- version);
- }
-
- public void revise(BigDecimal amount, LocalDate refundedAt) {
- requireDraft();
- this.amount = positive(amount);
- this.refundedAt = Objects.requireNonNull(refundedAt);
- }
-
- public RentPaymentRefunded confirm(RentPayment payment, LocalDate confirmationDate) {
- requireDraft();
- Objects.requireNonNull(payment);
- if (!landlordId.equals(payment.landlordId()) || !leaseId.equals(payment.leaseId())
- || !paymentId.equals(payment.id())) {
- throw new RentRuleViolation("Le remboursement et le règlement doivent appartenir au même bail.");
- }
- payment.registerRefund(amount);
- confirmedAt = Objects.requireNonNull(confirmationDate);
- status = RentPaymentRefundStatus.CONFIRMED;
- return new RentPaymentRefunded(id, landlordId, leaseId, paymentId, amount, refundedAt, confirmedAt);
- }
-
- private void requireDraft() {
- if (status != RentPaymentRefundStatus.DRAFT) {
- throw new RentRuleViolation("Un remboursement confirmé est immuable.");
- }
+ return new RentPaymentRefund(id, landlordId, leaseId, paymentId, amount, refundedAt, version);
}
private static BigDecimal positive(BigDecimal amount) {
@@ -92,7 +59,5 @@ public final class RentPaymentRefund {
public RentPaymentId paymentId() { return paymentId; }
public BigDecimal amount() { return amount; }
public LocalDate refundedAt() { return refundedAt; }
- public RentPaymentRefundStatus status() { return status; }
- public LocalDate confirmedAt() { return confirmedAt; }
public long version() { return version; }
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundRepository.java
index e4f8414..61cc27d 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundRepository.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundRepository.java
@@ -6,11 +6,11 @@ import java.util.List;
import java.util.Optional;
public interface RentPaymentRefundRepository {
+ List findAll(LandlordId landlordId);
+
List findAll(LandlordId landlordId, RentPaymentId paymentId);
Optional find(LandlordId landlordId, RentPaymentRefundId refundId);
RentPaymentRefund save(RentPaymentRefund refund);
-
- boolean deleteDraft(RentPaymentRefund refund);
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundStatus.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundStatus.java
deleted file mode 100644
index 76dc759..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefundStatus.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package fr.nessar.lmnporganizer.rental.domain.rent;
-
-public enum RentPaymentRefundStatus {
- DRAFT,
- CONFIRMED
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefunded.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefunded.java
deleted file mode 100644
index f896ab6..0000000
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRefunded.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package fr.nessar.lmnporganizer.rental.domain.rent;
-
-import fr.nessar.lmnporganizer.rental.domain.landlord.LandlordId;
-import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-
-public record RentPaymentRefunded(RentPaymentRefundId refundId, LandlordId landlordId, LeaseId leaseId,
- RentPaymentId paymentId, BigDecimal amount, LocalDate refundedAt,
- LocalDate confirmedAt) {
-}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRepository.java
index b12f48f..62b9b32 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRepository.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentRepository.java
@@ -14,6 +14,4 @@ public interface RentPaymentRepository {
Optional find(LandlordId landlordId, RentPaymentId paymentId);
RentPayment save(RentPayment payment);
-
- boolean deleteDraft(RentPayment payment);
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentReversal.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentReversal.java
index 7624df9..e09b76b 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentReversal.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentReversal.java
@@ -22,17 +22,25 @@ public record RentPaymentReversal(
Objects.requireNonNull(leaseId, "Lease ID is required.");
Objects.requireNonNull(paymentId, "Rent payment ID is required.");
Objects.requireNonNull(reversedAt, "Reversal date is required.");
- if (reason == null || reason.isBlank()) {
- throw new RentRuleViolation("La raison de la contrepassation est obligatoire.");
- }
- reason = reason.trim();
+ reason = validatedReason(reason);
reversedAllocations = List.copyOf(reversedAllocations);
}
public static RentPaymentReversal reverse(RentPaymentReversalId id, RentPayment payment, LocalDate reversedAt,
String reason) {
Objects.requireNonNull(payment);
- return new RentPaymentReversal(id, payment.landlordId(), payment.leaseId(), payment.id(), reversedAt, reason,
- payment.reverse());
+ return new RentPaymentReversal(id, payment.landlordId(), payment.leaseId(), payment.id(), reversedAt,
+ validatedReason(reason), payment.reverse());
+ }
+
+ private static String validatedReason(String reason) {
+ if (reason == null || reason.isBlank()) {
+ throw new RentRuleViolation("La raison de la contrepassation est obligatoire.");
+ }
+ String validated = reason.trim();
+ if (validated.length() > 500) {
+ throw new RentRuleViolation("La raison de la contrepassation ne peut pas dépasser 500 caractères.");
+ }
+ return validated;
}
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentStatus.java b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentStatus.java
index 06a637d..b4c26c5 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentStatus.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/rental/domain/rent/RentPaymentStatus.java
@@ -1,7 +1,6 @@
package fr.nessar.lmnporganizer.rental.domain.rent;
public enum RentPaymentStatus {
- DRAFT,
CONFIRMED,
REVERSED
}
diff --git a/backend/src/main/resources/templates/rental/payment-receipt.html b/backend/src/main/resources/templates/rental/payment-receipt.html
index 746ee10..0f75c05 100644
--- a/backend/src/main/resources/templates/rental/payment-receipt.html
+++ b/backend/src/main/resources/templates/rental/payment-receipt.html
@@ -37,15 +37,14 @@
Remboursements
- | Référence | Montant | Date | Confirmé le |
+ | Référence | Montant | Date |
| refund-1 |
100.00 EUR |
2026-07-06 |
- 2026-07-07 |
- | Aucun remboursement |
+ | Aucun remboursement |