Keep revision violations domain-shaped (#56)
This commit is contained in:
1 parent
41b086fb33
commit
fe36d6cae4
5 files changed
+119
-31
No files matched your search
+26
-10
@@ -56,10 +56,7 @@ ResponseEntity<RentRevisionApi.ValidationError> invalid(RentRevisionValidationFa
|
||||
return ResponseEntity.badRequest()
|
||||
.body(
|
||||
new RentRevisionApi.ValidationError(
|
||||
"validation_failed",
|
||||
failure.errors().stream()
|
||||
.map(error -> new RentRevisionApi.FieldError(error.field(), error.code()))
|
||||
.toList()));
|
||||
"validation_failed", List.of(fieldError(failure.rule()))));
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
@@ -184,25 +181,30 @@ private static RentRevisionService.PdfUpload upload(MultipartFile file) {
|
||||
}
|
||||
|
||||
private static RentRevision.DeliveryMethod deliveryMethod(String value) {
|
||||
if (value == null) throw new RentRevisionValidationFailure("deliveryMethod", "invalid");
|
||||
if (value == null) {
|
||||
throw new RentRevisionValidationFailure(
|
||||
RentRevisionValidationFailure.Rule.INVALID_DELIVERY_METHOD);
|
||||
}
|
||||
return switch (value.trim()) {
|
||||
case "REGISTERED_LETTER" -> RentRevision.DeliveryMethod.REGISTERED_LETTER;
|
||||
case "POSTAL_LETTER" -> RentRevision.DeliveryMethod.POSTAL_LETTER;
|
||||
case "HAND_DELIVERY" -> RentRevision.DeliveryMethod.HAND_DELIVERY;
|
||||
case "EMAIL" -> RentRevision.DeliveryMethod.EMAIL;
|
||||
case "OTHER" -> RentRevision.DeliveryMethod.OTHER;
|
||||
default -> throw new RentRevisionValidationFailure("deliveryMethod", "invalid");
|
||||
default ->
|
||||
throw new RentRevisionValidationFailure(
|
||||
RentRevisionValidationFailure.Rule.INVALID_DELIVERY_METHOD);
|
||||
};
|
||||
}
|
||||
|
||||
static ZoneId timeZone(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new RentRevisionValidationFailure("timeZone", "iana_time_zone");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.INVALID_TIME_ZONE);
|
||||
}
|
||||
try {
|
||||
return ZoneId.of(value);
|
||||
} catch (DateTimeException failure) {
|
||||
throw new RentRevisionValidationFailure("timeZone", "iana_time_zone");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.INVALID_TIME_ZONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,14 +212,28 @@ private static LocalDate date(String value) {
|
||||
try {
|
||||
return LocalDate.parse(value);
|
||||
} catch (DateTimeParseException failure) {
|
||||
throw new RentRevisionValidationFailure("sentOn", "invalid");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.INVALID_SENT_ON);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean deliveryConfirmed(String value) {
|
||||
if ("true".equals(value)) return true;
|
||||
if ("false".equals(value)) return false;
|
||||
throw new RentRevisionValidationFailure("deliveryConfirmed", "invalid");
|
||||
throw new RentRevisionValidationFailure(
|
||||
RentRevisionValidationFailure.Rule.INVALID_DELIVERY_CONFIRMATION);
|
||||
}
|
||||
|
||||
private static RentRevisionApi.FieldError fieldError(RentRevisionValidationFailure.Rule rule) {
|
||||
return switch (rule) {
|
||||
case INVALID_DELIVERY_METHOD -> new RentRevisionApi.FieldError("deliveryMethod", "invalid");
|
||||
case INVALID_TIME_ZONE -> new RentRevisionApi.FieldError("timeZone", "iana_time_zone");
|
||||
case INVALID_SENT_ON -> new RentRevisionApi.FieldError("sentOn", "invalid");
|
||||
case INVALID_DELIVERY_CONFIRMATION ->
|
||||
new RentRevisionApi.FieldError("deliveryConfirmed", "invalid");
|
||||
case DELIVERY_CONFIRMATION_REQUIRED ->
|
||||
new RentRevisionApi.FieldError("deliveryConfirmed", "required");
|
||||
case SENT_ON_IN_FUTURE -> new RentRevisionApi.FieldError("sentOn", "future");
|
||||
case INVALID_PROOF -> new RentRevisionApi.FieldError("proof", "invalid");
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -213,11 +213,12 @@ public ApplyResult apply(
|
||||
Objects.requireNonNull(request, "Rent revision application request is required.");
|
||||
String author = required(authorId, "Application author");
|
||||
if (!request.deliveryConfirmed()) {
|
||||
throw new RentRevisionValidationFailure("deliveryConfirmed", "required");
|
||||
throw new RentRevisionValidationFailure(
|
||||
RentRevisionValidationFailure.Rule.DELIVERY_CONFIRMATION_REQUIRED);
|
||||
}
|
||||
if (request.sentOn() == null
|
||||
|| request.sentOn().isAfter(LocalDate.now(clock.withZone(request.timeZone())))) {
|
||||
throw new RentRevisionValidationFailure("sentOn", "future");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.SENT_ON_IN_FUTURE);
|
||||
}
|
||||
RentRevision proposed =
|
||||
revisions
|
||||
@@ -638,7 +639,7 @@ private static String sha256(byte[] content) {
|
||||
private byte[] pdf(PdfUpload upload) {
|
||||
byte[] content = upload.content();
|
||||
if (content == null || content.length == 0) {
|
||||
throw new RentRevisionValidationFailure("proof", "invalid");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.INVALID_PROOF);
|
||||
}
|
||||
if (content.length > maximumPdfSize) {
|
||||
throw new AttachmentTooLarge(maximumPdfSize);
|
||||
@@ -646,7 +647,7 @@ private byte[] pdf(PdfUpload upload) {
|
||||
if (!"application/pdf".equalsIgnoreCase(upload.contentType())
|
||||
|| content.length < 5
|
||||
|| !Arrays.equals(Arrays.copyOf(content, 5), "%PDF-".getBytes(StandardCharsets.US_ASCII))) {
|
||||
throw new RentRevisionValidationFailure("proof", "invalid");
|
||||
throw new RentRevisionValidationFailure(RentRevisionValidationFailure.Rule.INVALID_PROOF);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
+13
-11
@@ -1,24 +1,26 @@
|
||||
package fr.nessar.lmnporganizer.rental.application.rent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class RentRevisionValidationFailure extends RuntimeException {
|
||||
private final List<FieldError> errors;
|
||||
private final Rule rule;
|
||||
|
||||
public RentRevisionValidationFailure(String field, String code) {
|
||||
public RentRevisionValidationFailure(Rule rule) {
|
||||
super("La requête de révision IRL est invalide.");
|
||||
this.errors = List.of(new FieldError(field, code));
|
||||
this.rule = Objects.requireNonNull(rule);
|
||||
}
|
||||
|
||||
public List<FieldError> errors() {
|
||||
return errors;
|
||||
public Rule rule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public record FieldError(String field, String code) {
|
||||
public FieldError {
|
||||
Objects.requireNonNull(field);
|
||||
Objects.requireNonNull(code);
|
||||
}
|
||||
public enum Rule {
|
||||
INVALID_DELIVERY_METHOD,
|
||||
INVALID_TIME_ZONE,
|
||||
INVALID_SENT_ON,
|
||||
INVALID_DELIVERY_CONFIRMATION,
|
||||
DELIVERY_CONFIRMATION_REQUIRED,
|
||||
SENT_ON_IN_FUTURE,
|
||||
INVALID_PROOF
|
||||
}
|
||||
}
|
||||
+41
-6
@@ -165,21 +165,52 @@ void rejectsAnUnknownDeliveryMethodWithAStableFieldCode() throws Exception {
|
||||
.andExpect(jsonPath("$.errors[0].code").value("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidApplicationDatesAndDeliveryConfirmationsWithStableFieldCodes()
|
||||
throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
multipart("/api/leases/lease-1/rent-revisions/revision-1/apply")
|
||||
.param("timeZone", "Europe/Paris")
|
||||
.param("sentOn", "not-a-date")
|
||||
.param("deliveryMethod", "POSTAL_LETTER")
|
||||
.param("deliveryConfirmed", "true")
|
||||
.with(owner())
|
||||
.with(csrf()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(jsonPath("$.errors[0].field").value("sentOn"))
|
||||
.andExpect(jsonPath("$.errors[0].code").value("invalid"));
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
multipart("/api/leases/lease-1/rent-revisions/revision-1/apply")
|
||||
.param("timeZone", "Europe/Paris")
|
||||
.param("sentOn", "2027-09-02")
|
||||
.param("deliveryMethod", "POSTAL_LETTER")
|
||||
.param("deliveryConfirmed", "yes")
|
||||
.with(owner())
|
||||
.with(csrf()))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(jsonPath("$.errors[0].field").value("deliveryConfirmed"))
|
||||
.andExpect(jsonPath("$.errors[0].code").value("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void translatesInvalidDeliveryFactsAndProofMetadataToFieldErrors() throws Exception {
|
||||
assertApplyViolation(
|
||||
"false",
|
||||
null,
|
||||
RentRevisionValidationFailure.Rule.DELIVERY_CONFIRMATION_REQUIRED,
|
||||
"deliveryConfirmed",
|
||||
"required");
|
||||
assertApplyViolation(
|
||||
"true",
|
||||
null,
|
||||
"sentOn",
|
||||
"future");
|
||||
"true", null, RentRevisionValidationFailure.Rule.SENT_ON_IN_FUTURE, "sentOn", "future");
|
||||
assertApplyViolation(
|
||||
"true",
|
||||
new MockMultipartFile("proof", "preuve.pdf", "application/pdf", "not a PDF".getBytes()),
|
||||
RentRevisionValidationFailure.Rule.INVALID_PROOF,
|
||||
"proof",
|
||||
"invalid");
|
||||
}
|
||||
@@ -394,9 +425,13 @@ private static RequestPostProcessor owner() {
|
||||
}
|
||||
|
||||
private void assertApplyViolation(
|
||||
String deliveryConfirmed, MockMultipartFile proof, String field, String code)
|
||||
String deliveryConfirmed,
|
||||
MockMultipartFile proof,
|
||||
RentRevisionValidationFailure.Rule rule,
|
||||
String field,
|
||||
String code)
|
||||
throws Exception {
|
||||
doThrow(new RentRevisionValidationFailure(field, code))
|
||||
doThrow(new RentRevisionValidationFailure(rule))
|
||||
.when(revisions)
|
||||
.apply(any(), any(), any(), any(), any());
|
||||
var request = multipart("/api/leases/lease-1/rent-revisions/revision-1/apply");
|
||||
|
||||
+34
@@ -356,6 +356,40 @@ void refusesToRenderOrStoreAProposalWhoseEffectiveDateIsAfterTheLeaseEnd() {
|
||||
assertEquals(List.of(), revisionRepository.values);
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifiesApplicationValidationRulesWithoutWebDetails() {
|
||||
RentRevisionService service =
|
||||
new RentRevisionService(
|
||||
mock(LeaseRepository.class),
|
||||
mock(IrlIndexProvider.class),
|
||||
mock(RentRevisionRepository.class),
|
||||
mock(RentDueRepository.class),
|
||||
mock(RentalDocumentRenderer.class),
|
||||
mock(LeaseDocumentStorage.class),
|
||||
directTransactions(),
|
||||
() -> "revision-1",
|
||||
Clock.fixed(Instant.parse("2027-09-02T10:00:00Z"), ZoneOffset.UTC),
|
||||
1024);
|
||||
|
||||
RentRevisionValidationFailure failure =
|
||||
assertThrows(
|
||||
RentRevisionValidationFailure.class,
|
||||
() ->
|
||||
service.apply(
|
||||
LANDLORD_ID,
|
||||
LEASE_ID,
|
||||
new RentRevisionId("revision-1"),
|
||||
new RentRevisionService.ApplyRequest(
|
||||
ZoneId.of("Europe/Paris"),
|
||||
LocalDate.of(2027, 9, 2),
|
||||
RentRevision.DeliveryMethod.POSTAL_LETTER,
|
||||
false,
|
||||
null),
|
||||
"oidc:user-1"));
|
||||
|
||||
assertEquals(RentRevisionValidationFailure.Rule.DELIVERY_CONFIRMATION_REQUIRED, failure.rule());
|
||||
}
|
||||
|
||||
@Test
|
||||
void proposesFromTheFinancialTermsEffectiveOnTheRequestDateAfterAnAmendment() {
|
||||
Lease lease = leaseWithFinancialAmendmentAfterAnniversary();
|
||||
|
||||
Reference in new issue
Block a user