Keep lease-ending violations domain-shaped (#54)
This commit is contained in:
1 parent
603b508aec
commit
9054cc62d3
10 files changed
+213
-38
No files matched your search
+70
-8
@@ -1,6 +1,7 @@
|
||||
package fr.nessar.lmnporganizer.rental.adapter.in.web.lease;
|
||||
|
||||
import fr.nessar.lmnporganizer.rental.adapter.in.web.common.AuthenticatedLandlord;
|
||||
import fr.nessar.lmnporganizer.rental.application.lease.LeaseEndingApplicationValidationFailure;
|
||||
import fr.nessar.lmnporganizer.rental.application.lease.LeaseEndingService;
|
||||
import fr.nessar.lmnporganizer.rental.application.shared.AttachmentStorageFailure;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseEndingConflict;
|
||||
@@ -59,13 +60,19 @@ ResponseEntity<LeaseEndingApi.OperationError> conflict(LeaseEndingConflict failu
|
||||
|
||||
@ExceptionHandler(LeaseEndingValidationFailure.class)
|
||||
ResponseEntity<LeaseEndingApi.ValidationError> invalid(LeaseEndingValidationFailure failure) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(
|
||||
new LeaseEndingApi.ValidationError(
|
||||
"validation_failed",
|
||||
failure.errors().stream()
|
||||
.map(error -> new LeaseEndingApi.FieldError(error.field(), error.code()))
|
||||
.toList()));
|
||||
return validationFailure(domainError(failure.rule()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(LeaseEndingApplicationValidationFailure.class)
|
||||
ResponseEntity<LeaseEndingApi.ValidationError> invalid(
|
||||
LeaseEndingApplicationValidationFailure failure) {
|
||||
return validationFailure(applicationError(failure.rule()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(LeaseEndingRequestValidationFailure.class)
|
||||
ResponseEntity<LeaseEndingApi.ValidationError> invalid(
|
||||
LeaseEndingRequestValidationFailure failure) {
|
||||
return validationFailure(failure.error());
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
@@ -242,10 +249,53 @@ private static ZoneId zone(String value) {
|
||||
try {
|
||||
return ZoneId.of(value);
|
||||
} catch (DateTimeException failure) {
|
||||
throw new LeaseEndingValidationFailure("timeZone", "iana_time_zone");
|
||||
throw new LeaseEndingRequestValidationFailure("timeZone", "iana_time_zone");
|
||||
}
|
||||
}
|
||||
|
||||
private static ResponseEntity<LeaseEndingApi.ValidationError> validationFailure(
|
||||
LeaseEndingApi.FieldError error) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new LeaseEndingApi.ValidationError("validation_failed", java.util.List.of(error)));
|
||||
}
|
||||
|
||||
private static LeaseEndingApi.FieldError domainError(LeaseEndingValidationFailure.Rule rule) {
|
||||
return switch (rule) {
|
||||
case RECEIVED_ON_IN_FUTURE -> new LeaseEndingApi.FieldError("receivedOn", "not_future");
|
||||
case LATE_REGISTRATION_NOT_CONFIRMED ->
|
||||
new LeaseEndingApi.FieldError("lateRegistrationConfirmed", "required");
|
||||
case REQUESTED_DEPARTURE_BEFORE_RECEIPT ->
|
||||
new LeaseEndingApi.FieldError("requestedDepartureOn", "after_or_equal_received_on");
|
||||
case NON_STANDARD_DELIVERY_NOT_CONFIRMED ->
|
||||
new LeaseEndingApi.FieldError("nonStandardDeliveryConfirmed", "required");
|
||||
case OTHER_DELIVERY_MODE_TOO_LONG ->
|
||||
new LeaseEndingApi.FieldError("otherDeliveryMode", "max_length");
|
||||
case OTHER_DELIVERY_MODE_MISSING ->
|
||||
new LeaseEndingApi.FieldError("otherDeliveryMode", "required");
|
||||
case OTHER_DELIVERY_MODE_UNEXPECTED ->
|
||||
new LeaseEndingApi.FieldError("otherDeliveryMode", "forbidden");
|
||||
case WITHDRAWAL_ON_OUTSIDE_NOTICE_PERIOD ->
|
||||
new LeaseEndingApi.FieldError("acceptedOn", "between_notice_received_on_and_today");
|
||||
case INVALIDATION_ON_OUTSIDE_NOTICE_PERIOD ->
|
||||
new LeaseEndingApi.FieldError("invalidatedOn", "between_notice_received_on_and_today");
|
||||
case AGREED_END_BEFORE_LEASE_START ->
|
||||
new LeaseEndingApi.FieldError("endOn", "after_or_equal_lease_start");
|
||||
case AGREED_END_DOES_NOT_ADVANCE_CURRENT_END ->
|
||||
new LeaseEndingApi.FieldError("endOn", "before_current_end");
|
||||
case REASON_REQUIRED -> new LeaseEndingApi.FieldError("reason", "required");
|
||||
case REASON_TOO_LONG -> new LeaseEndingApi.FieldError("reason", "max_length");
|
||||
};
|
||||
}
|
||||
|
||||
private static LeaseEndingApi.FieldError applicationError(
|
||||
LeaseEndingApplicationValidationFailure.Rule rule) {
|
||||
return switch (rule) {
|
||||
case MISSING_EVIDENCE_CONFIRMATION ->
|
||||
new LeaseEndingApi.FieldError("missingEvidenceConfirmed", "required");
|
||||
case EMPTY_EVIDENCE -> new LeaseEndingApi.FieldError("evidence", "empty");
|
||||
};
|
||||
}
|
||||
|
||||
private static String safeFileName(String value) {
|
||||
String normalized = value == null ? "justificatif" : value.replace('\\', '/');
|
||||
String leaf = normalized.substring(normalized.lastIndexOf('/') + 1).trim();
|
||||
@@ -274,4 +324,16 @@ record AgreedEndRequest(
|
||||
@NotBlank @Size(max = 500) String reason) {}
|
||||
|
||||
record ClosureRequest(@NotBlank String timeZone, @AssertTrue boolean confirmed) {}
|
||||
|
||||
private static final class LeaseEndingRequestValidationFailure extends RuntimeException {
|
||||
private final LeaseEndingApi.FieldError error;
|
||||
|
||||
private LeaseEndingRequestValidationFailure(String field, String code) {
|
||||
this.error = new LeaseEndingApi.FieldError(field, code);
|
||||
}
|
||||
|
||||
private LeaseEndingApi.FieldError error() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.nessar.lmnporganizer.rental.application.lease;
|
||||
|
||||
public final class LeaseEndingApplicationValidationFailure extends RuntimeException {
|
||||
private final Rule rule;
|
||||
|
||||
public LeaseEndingApplicationValidationFailure(Rule rule) {
|
||||
super("La demande de fin de bail est invalide.");
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
public Rule rule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public enum Rule {
|
||||
MISSING_EVIDENCE_CONFIRMATION,
|
||||
EMPTY_EVIDENCE
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -15,7 +15,6 @@ import fr.nessar.lmnporganizer.rental.domain.lease.ApartmentOccupancyRepository;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.Lease;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseActionAudit;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseEndingConflict;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseEndingValidationFailure;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseId;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.rental.domain.lease.LeaseRuleViolation;
|
||||
@@ -94,7 +93,8 @@ public LeaseEndingState recordNotice(
|
||||
EvidenceUpload evidence) {
|
||||
Objects.requireNonNull(request);
|
||||
if (evidence == null && !request.missingEvidenceConfirmed()) {
|
||||
throw new LeaseEndingValidationFailure("missingEvidenceConfirmed", "required");
|
||||
throw new LeaseEndingApplicationValidationFailure(
|
||||
LeaseEndingApplicationValidationFailure.Rule.MISSING_EVIDENCE_CONFIRMATION);
|
||||
}
|
||||
TenantNoticeId noticeId = new TenantNoticeId(identifiers.next());
|
||||
StoredEvidence stored = evidence == null ? null : store(evidence);
|
||||
@@ -343,7 +343,8 @@ private StoredEvidence store(EvidenceUpload upload) {
|
||||
byte[] content =
|
||||
Objects.requireNonNull(upload.content(), "Evidence content is required.").clone();
|
||||
if (content.length == 0) {
|
||||
throw new LeaseEndingValidationFailure("evidence", "empty");
|
||||
throw new LeaseEndingApplicationValidationFailure(
|
||||
LeaseEndingApplicationValidationFailure.Rule.EMPTY_EVIDENCE);
|
||||
}
|
||||
if (content.length > maxEvidenceBytes) {
|
||||
throw new AttachmentTooLarge(maxEvidenceBytes);
|
||||
|
||||
@@ -507,17 +507,21 @@ public TenantNotice recordTenantNotice(TenantNoticeCommand command, LeaseActionA
|
||||
Objects.requireNonNull(command);
|
||||
Objects.requireNonNull(audit);
|
||||
if (command.receivedOn().isAfter(command.today())) {
|
||||
throw new LeaseEndingValidationFailure("receivedOn", "not_future");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.RECEIVED_ON_IN_FUTURE);
|
||||
}
|
||||
if (command.receivedOn().isBefore(command.today()) && !command.lateRegistrationConfirmed()) {
|
||||
throw new LeaseEndingValidationFailure("lateRegistrationConfirmed", "required");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.LATE_REGISTRATION_NOT_CONFIRMED);
|
||||
}
|
||||
if (command.requestedDepartureOn().isBefore(command.receivedOn())) {
|
||||
throw new LeaseEndingValidationFailure("requestedDepartureOn", "after_or_equal_received_on");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.REQUESTED_DEPARTURE_BEFORE_RECEIPT);
|
||||
}
|
||||
if (command.deliveryMode() == NoticeDeliveryMode.OTHER
|
||||
&& !command.nonStandardDeliveryConfirmed()) {
|
||||
throw new LeaseEndingValidationFailure("nonStandardDeliveryConfirmed", "required");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.NON_STANDARD_DELIVERY_NOT_CONFIRMED);
|
||||
}
|
||||
TenantNotice notice =
|
||||
new TenantNotice(
|
||||
@@ -546,7 +550,11 @@ public NoticeWithdrawal acceptNoticeWithdrawal(
|
||||
endingHistory
|
||||
.notice(noticeId)
|
||||
.orElseThrow(() -> new LeaseRuleViolation("Le congé locataire est introuvable."));
|
||||
validateAdministrativeFactDate(acceptedOn, today, notice.receivedOn(), "acceptedOn");
|
||||
validateAdministrativeFactDate(
|
||||
acceptedOn,
|
||||
today,
|
||||
notice.receivedOn(),
|
||||
LeaseEndingValidationFailure.Rule.WITHDRAWAL_ON_OUTSIDE_NOTICE_PERIOD);
|
||||
return endingHistory.withdraw(noticeId, acceptedOn, reason, audit);
|
||||
}
|
||||
|
||||
@@ -561,7 +569,11 @@ public NoticeInvalidation invalidateNotice(
|
||||
endingHistory
|
||||
.notice(noticeId)
|
||||
.orElseThrow(() -> new LeaseRuleViolation("Le congé locataire est introuvable."));
|
||||
validateAdministrativeFactDate(invalidatedOn, today, notice.receivedOn(), "invalidatedOn");
|
||||
validateAdministrativeFactDate(
|
||||
invalidatedOn,
|
||||
today,
|
||||
notice.receivedOn(),
|
||||
LeaseEndingValidationFailure.Rule.INVALIDATION_ON_OUTSIDE_NOTICE_PERIOD);
|
||||
return endingHistory.invalidate(noticeId, invalidatedOn, reason, audit);
|
||||
}
|
||||
|
||||
@@ -570,11 +582,13 @@ public AgreedEarlyLeaseEnd recordAgreedEarlyEnd(
|
||||
requireActiveEndingSupported();
|
||||
Objects.requireNonNull(endOn, "Agreed end date is required.");
|
||||
if (endOn.isBefore(period.startDate())) {
|
||||
throw new LeaseEndingValidationFailure("endOn", "after_or_equal_lease_start");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.AGREED_END_BEFORE_LEASE_START);
|
||||
}
|
||||
Optional<LocalDate> currentEnd = contractEndOn();
|
||||
if (currentEnd.isPresent() && !endOn.isBefore(currentEnd.orElseThrow())) {
|
||||
throw new LeaseEndingValidationFailure("endOn", "before_current_end");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.AGREED_END_DOES_NOT_ADVANCE_CURRENT_END);
|
||||
}
|
||||
return endingHistory.agree(endOn, reason, audit);
|
||||
}
|
||||
@@ -694,11 +708,14 @@ private void requireEndingSupported() {
|
||||
}
|
||||
|
||||
private static void validateAdministrativeFactDate(
|
||||
LocalDate factOn, LocalDate today, LocalDate noticeReceivedOn, String field) {
|
||||
LocalDate factOn,
|
||||
LocalDate today,
|
||||
LocalDate noticeReceivedOn,
|
||||
LeaseEndingValidationFailure.Rule rule) {
|
||||
Objects.requireNonNull(factOn, "Administrative fact date is required.");
|
||||
Objects.requireNonNull(today, "Current civil date is required.");
|
||||
if (factOn.isBefore(noticeReceivedOn) || factOn.isAfter(today)) {
|
||||
throw new LeaseEndingValidationFailure(field, "between_notice_received_on_and_today");
|
||||
throw new LeaseEndingValidationFailure(rule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -215,11 +215,11 @@ private void requireKnownNotice(TenantNoticeId noticeId) {
|
||||
|
||||
static String requiredReason(String reason) {
|
||||
if (reason == null || reason.isBlank()) {
|
||||
throw new LeaseEndingValidationFailure("reason", "required");
|
||||
throw new LeaseEndingValidationFailure(LeaseEndingValidationFailure.Rule.REASON_REQUIRED);
|
||||
}
|
||||
String value = reason.trim();
|
||||
if (value.length() > 500) {
|
||||
throw new LeaseEndingValidationFailure("reason", "max_length");
|
||||
throw new LeaseEndingValidationFailure(LeaseEndingValidationFailure.Rule.REASON_TOO_LONG);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
+20
-8
@@ -1,18 +1,30 @@
|
||||
package fr.nessar.lmnporganizer.rental.domain.lease;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class LeaseEndingValidationFailure extends RuntimeException {
|
||||
private final List<FieldError> errors;
|
||||
private final Rule rule;
|
||||
|
||||
public LeaseEndingValidationFailure(String field, String code) {
|
||||
public LeaseEndingValidationFailure(Rule rule) {
|
||||
super("La demande de fin de bail est invalide.");
|
||||
this.errors = List.of(new FieldError(field, code));
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
public List<FieldError> errors() {
|
||||
return errors;
|
||||
public Rule rule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public record FieldError(String field, String code) {}
|
||||
public enum Rule {
|
||||
RECEIVED_ON_IN_FUTURE,
|
||||
LATE_REGISTRATION_NOT_CONFIRMED,
|
||||
REQUESTED_DEPARTURE_BEFORE_RECEIPT,
|
||||
NON_STANDARD_DELIVERY_NOT_CONFIRMED,
|
||||
OTHER_DELIVERY_MODE_TOO_LONG,
|
||||
OTHER_DELIVERY_MODE_MISSING,
|
||||
OTHER_DELIVERY_MODE_UNEXPECTED,
|
||||
WITHDRAWAL_ON_OUTSIDE_NOTICE_PERIOD,
|
||||
INVALIDATION_ON_OUTSIDE_NOTICE_PERIOD,
|
||||
AGREED_END_BEFORE_LEASE_START,
|
||||
AGREED_END_DOES_NOT_ADVANCE_CURRENT_END,
|
||||
REASON_REQUIRED,
|
||||
REASON_TOO_LONG
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,16 @@ public TenantNotice {
|
||||
ruleVersion = ruleVersion.trim();
|
||||
otherDeliveryMode = optional(otherDeliveryMode);
|
||||
if (otherDeliveryMode != null && otherDeliveryMode.length() > 200) {
|
||||
throw new LeaseEndingValidationFailure("otherDeliveryMode", "max_length");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.OTHER_DELIVERY_MODE_TOO_LONG);
|
||||
}
|
||||
if (deliveryMode == NoticeDeliveryMode.OTHER && otherDeliveryMode == null) {
|
||||
throw new LeaseEndingValidationFailure("otherDeliveryMode", "required");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.OTHER_DELIVERY_MODE_MISSING);
|
||||
}
|
||||
if (deliveryMode != NoticeDeliveryMode.OTHER && otherDeliveryMode != null) {
|
||||
throw new LeaseEndingValidationFailure("otherDeliveryMode", "forbidden");
|
||||
throw new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.OTHER_DELIVERY_MODE_UNEXPECTED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-1
@@ -20,6 +20,7 @@ import fr.nessar.lmnporganizer.identity.adapter.in.security.SecurityConfiguratio
|
||||
import fr.nessar.lmnporganizer.rental.adapter.in.web.common.AuthenticatedLandlord;
|
||||
import fr.nessar.lmnporganizer.rental.adapter.in.web.common.RentalApiExceptionHandler;
|
||||
import fr.nessar.lmnporganizer.rental.application.landlord.LandlordAccountService;
|
||||
import fr.nessar.lmnporganizer.rental.application.lease.LeaseEndingApplicationValidationFailure;
|
||||
import fr.nessar.lmnporganizer.rental.application.lease.LeaseEndingService;
|
||||
import fr.nessar.lmnporganizer.rental.application.lease.LeaseEndingState;
|
||||
import fr.nessar.lmnporganizer.rental.domain.apartment.ApartmentId;
|
||||
@@ -143,7 +144,9 @@ void lifecycleConflictsRemainDistinctFromRequestValidation() throws Exception {
|
||||
@Test
|
||||
void domainValidationUsesStableFieldPathsAndCodes() throws Exception {
|
||||
when(endings.recordNotice(any(), any(), any(), any(), any()))
|
||||
.thenThrow(new LeaseEndingValidationFailure("receivedOn", "not_future"));
|
||||
.thenThrow(
|
||||
new LeaseEndingValidationFailure(
|
||||
LeaseEndingValidationFailure.Rule.RECEIVED_ON_IN_FUTURE));
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
@@ -163,6 +166,38 @@ void domainValidationUsesStableFieldPathsAndCodes() throws Exception {
|
||||
.andExpect(jsonPath("$.errors[0].code").value("not_future"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applicationValidationAndInvalidTimeZonesUseTheSameStableFieldContract() throws Exception {
|
||||
when(endings.recordNotice(any(), any(), any(), any(), any()))
|
||||
.thenThrow(
|
||||
new LeaseEndingApplicationValidationFailure(
|
||||
LeaseEndingApplicationValidationFailure.Rule.MISSING_EVIDENCE_CONFIRMATION));
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
multipart("/api/leases/lease-1/end/notices")
|
||||
.param("timeZone", "Europe/Paris")
|
||||
.param("receivedOn", "2026-07-19")
|
||||
.param("requestedDepartureOn", "2026-08-19")
|
||||
.param("deliveryMode", "REGISTERED_LETTER")
|
||||
.param("lateRegistrationConfirmed", "false")
|
||||
.param("nonStandardDeliveryConfirmed", "false")
|
||||
.param("missingEvidenceConfirmed", "false")
|
||||
.with(landlord())
|
||||
.with(csrf()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(jsonPath("$.errors[0].field").value("missingEvidenceConfirmed"))
|
||||
.andExpect(jsonPath("$.errors[0].code").value("required"));
|
||||
|
||||
mockMvc
|
||||
.perform(get("/api/leases/lease-1/end").param("timeZone", "Mars/Olympus").with(landlord()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(jsonPath("$.errors[0].field").value("timeZone"))
|
||||
.andExpect(jsonPath("$.errors[0].code").value("iana_time_zone"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noticeMultipartForwardsTimezoneEvidenceAttestationAndAuthenticatedAuthor() throws Exception {
|
||||
when(endings.recordNotice(any(), any(), any(), any(), any())).thenReturn(state());
|
||||
|
||||
+27
-1
@@ -120,7 +120,7 @@ void noticeWithoutEvidenceRequiresTheExplicitAttestation() {
|
||||
storage);
|
||||
|
||||
assertThrows(
|
||||
LeaseEndingValidationFailure.class,
|
||||
LeaseEndingApplicationValidationFailure.class,
|
||||
() ->
|
||||
service.recordNotice(
|
||||
LANDLORD_ID,
|
||||
@@ -141,6 +141,32 @@ void noticeWithoutEvidenceRequiresTheExplicitAttestation() {
|
||||
verify(storage, never()).store(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyEvidenceIsAnApplicationValidationFailure() {
|
||||
LeaseDocumentStorage storage = mock(LeaseDocumentStorage.class);
|
||||
LeaseEndingService service =
|
||||
service(
|
||||
mock(LeaseRepository.class),
|
||||
mock(ApartmentOccupancyRepository.class),
|
||||
mock(RentDueRepository.class),
|
||||
storage);
|
||||
|
||||
LeaseEndingApplicationValidationFailure failure =
|
||||
assertThrows(
|
||||
LeaseEndingApplicationValidationFailure.class,
|
||||
() ->
|
||||
service.appendEvidence(
|
||||
LANDLORD_ID,
|
||||
LEASE_ID,
|
||||
new TenantNoticeId("notice-1"),
|
||||
"oidc:landlord-1",
|
||||
new LeaseEndingService.EvidenceUpload(
|
||||
PARIS, "conge.pdf", "application/pdf", new byte[0], null)));
|
||||
|
||||
assertEquals(LeaseEndingApplicationValidationFailure.Rule.EMPTY_EVIDENCE, failure.rule());
|
||||
verify(storage, never()).store(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void civilTodayIsComputedInTheLandlordSuppliedTimeZoneWithoutConvertingTheEnteredDate() {
|
||||
Clock boundaryClock = Clock.fixed(Instant.parse("2026-07-19T00:30:00Z"), ZoneOffset.UTC);
|
||||
|
||||
+2
-2
@@ -321,8 +321,8 @@ void agreedEarlyEndWorksWithoutNoticeAndCanShortenAnExistingNotice() {
|
||||
assertThrows(
|
||||
LeaseEndingValidationFailure.class,
|
||||
() -> lease.recordAgreedEarlyEnd(LocalDate.of(2026, 9, 30), "Sans effet", AUDIT));
|
||||
assertEquals("endOn", noChange.errors().getFirst().field());
|
||||
assertEquals("before_current_end", noChange.errors().getFirst().code());
|
||||
assertEquals(
|
||||
LeaseEndingValidationFailure.Rule.AGREED_END_DOES_NOT_ADVANCE_CURRENT_END, noChange.rule());
|
||||
assertThrows(
|
||||
LeaseEndingValidationFailure.class,
|
||||
() -> lease.recordAgreedEarlyEnd(LocalDate.of(2026, 10, 1), "Repousse la fin", AUDIT));
|
||||
|
||||
Reference in new issue
Block a user