Align optional apartment legal profile validation (#45)
This commit is contained in:
1 parent
72d3f0ccfa
commit
c1e7964f2e
10 files changed
+477
-56
No files matched your search
+34
-6
@@ -7,8 +7,6 @@ import fr.nessar.lmnporganizer.rental.domain.apartment.ApartmentLegalProfile.Con
|
||||
import fr.nessar.lmnporganizer.rental.domain.apartment.PrivateArea;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
@@ -31,16 +29,46 @@ record LegalProfileRequest(
|
||||
String hotWater,
|
||||
String energyRating,
|
||||
@PositiveOrZero BigDecimal estimatedAnnualEnergyCost,
|
||||
@Min(1900) @Max(2100) Integer estimatedAnnualEnergyCostReferenceYear,
|
||||
Integer estimatedAnnualEnergyCostReferenceYear,
|
||||
String destination,
|
||||
String privateAccessories,
|
||||
String commonAreas,
|
||||
String internetAccess,
|
||||
String departmentCode) {
|
||||
ApartmentLegalProfile toDomain() {
|
||||
if (estimatedAnnualEnergyCost != null && estimatedAnnualEnergyCostReferenceYear == null) {
|
||||
throw new IllegalArgumentException("L'année de référence du coût énergétique est requise.");
|
||||
@AssertTrue
|
||||
boolean isReferenceYearPresentWhenEnergyCostIsProvided() {
|
||||
return estimatedAnnualEnergyCost == null || estimatedAnnualEnergyCostReferenceYear != null;
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
boolean isReferenceYearWithinBoundsWhenEnergyCostIsProvided() {
|
||||
return estimatedAnnualEnergyCost == null
|
||||
|| estimatedAnnualEnergyCostReferenceYear == null
|
||||
|| (estimatedAnnualEnergyCostReferenceYear >= 1900
|
||||
&& estimatedAnnualEnergyCostReferenceYear <= 2100);
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
boolean isEnergyRatingValid() {
|
||||
return ApartmentLegalProfile.isValidEnergyRating(energyRating);
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
boolean isDepartmentCodeValid() {
|
||||
return ApartmentLegalProfile.isValidDepartmentCode(departmentCode);
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
boolean isConstructionPeriodValid() {
|
||||
try {
|
||||
ConstructionPeriod.fromCode(constructionPeriod);
|
||||
return true;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ApartmentLegalProfile toDomain() {
|
||||
return new ApartmentLegalProfile(
|
||||
taxIdentifier,
|
||||
buildingType,
|
||||
|
||||
+16
-8
@@ -37,25 +37,33 @@ public ApartmentLegalProfile {
|
||||
departmentCode = text(departmentCode).toUpperCase(Locale.ROOT);
|
||||
nonNegative(habitableSurface, "Habitable surface");
|
||||
nonNegative(estimatedAnnualEnergyCost, "Estimated annual energy cost");
|
||||
if (estimatedAnnualEnergyCost == null) {
|
||||
estimatedAnnualEnergyCostReferenceYear = null;
|
||||
}
|
||||
if (estimatedAnnualEnergyCostReferenceYear != null
|
||||
&& (estimatedAnnualEnergyCostReferenceYear < 1900
|
||||
|| estimatedAnnualEnergyCostReferenceYear > 2100)) {
|
||||
throw new IllegalArgumentException("Estimated annual energy cost reference year is invalid.");
|
||||
}
|
||||
if (estimatedAnnualEnergyCost == null) {
|
||||
estimatedAnnualEnergyCostReferenceYear = null;
|
||||
}
|
||||
if (!energyRating.isEmpty()
|
||||
&& (energyRating.length() != 1
|
||||
|| energyRating.charAt(0) < 'A'
|
||||
|| energyRating.charAt(0) > 'G')) {
|
||||
if (!isValidEnergyRating(energyRating)) {
|
||||
throw new IllegalArgumentException("Energy rating must be between A and G.");
|
||||
}
|
||||
if (!departmentCode.isEmpty() && !isDepartmentCode(departmentCode)) {
|
||||
if (!isValidDepartmentCode(departmentCode)) {
|
||||
throw new IllegalArgumentException("Department code is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isValidEnergyRating(String value) {
|
||||
String rating = text(value).toUpperCase(Locale.ROOT);
|
||||
return rating.isEmpty()
|
||||
|| (rating.length() == 1 && rating.charAt(0) >= 'A' && rating.charAt(0) <= 'G');
|
||||
}
|
||||
|
||||
public static boolean isValidDepartmentCode(String value) {
|
||||
String code = text(value).toUpperCase(Locale.ROOT);
|
||||
return code.isEmpty() || isDepartmentCode(code);
|
||||
}
|
||||
|
||||
public ApartmentLegalProfile(
|
||||
String taxIdentifier,
|
||||
BuildingType buildingType,
|
||||
|
||||
+87
@@ -813,6 +813,93 @@ void apartmentRequestsExposeEveryCoreValidationFailureWithStableFieldsAndCodes()
|
||||
jsonPath("$.errors[*].code", containsInAnyOrder("not_blank", "not_blank", "positive")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void apartmentRequestsKeepAnEmptyLegalProfileOptional() throws Exception {
|
||||
when(apartments.create(eq(LANDLORD_ID), any()))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
ApartmentCatalogService.Details details = invocation.getArgument(1);
|
||||
return new Apartment(
|
||||
APARTMENT_ID,
|
||||
LANDLORD_ID,
|
||||
details.name(),
|
||||
details.address(),
|
||||
details.purchasePrice(),
|
||||
details.description(),
|
||||
details.legalProfile(),
|
||||
details.privateAreas());
|
||||
});
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/apartments")
|
||||
.with(asLandlord())
|
||||
.with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"{\"name\":\"Studio\",\"address\":\"1 rue du Test\",\"purchasePrice\":1,\"legalProfile\":{},\"privateAreas\":[]}"))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void apartmentRequestsExposeEveryInvalidLegalProfileFieldWithStablePathsAndCodes()
|
||||
throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/apartments")
|
||||
.with(asLandlord())
|
||||
.with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"""
|
||||
{"name":"Studio","address":"1 rue du Test","purchasePrice":1,"legalProfile":{
|
||||
"constructionPeriod":"invalid","habitableSurface":-1,"energyRating":"Z",
|
||||
"estimatedAnnualEnergyCost":-1,"estimatedAnnualEnergyCostReferenceYear":1800,
|
||||
"departmentCode":"20"},"privateAreas":[]}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(
|
||||
jsonPath(
|
||||
"$.errors[*].field",
|
||||
containsInAnyOrder(
|
||||
"legalProfile.habitableSurface",
|
||||
"legalProfile.estimatedAnnualEnergyCost",
|
||||
"legalProfile.referenceYearWithinBoundsWhenEnergyCostIsProvided",
|
||||
"legalProfile.energyRatingValid",
|
||||
"legalProfile.departmentCodeValid",
|
||||
"legalProfile.constructionPeriodValid")))
|
||||
.andExpect(
|
||||
jsonPath(
|
||||
"$.errors[*].code",
|
||||
containsInAnyOrder(
|
||||
"positive_or_zero",
|
||||
"positive_or_zero",
|
||||
"assert_true",
|
||||
"assert_true",
|
||||
"assert_true",
|
||||
"assert_true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void apartmentRequestsRequireAnEnergyReferenceYearOnlyWhenACostIsProvided() throws Exception {
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/apartments")
|
||||
.with(asLandlord())
|
||||
.with(csrf())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"{\"name\":\"Studio\",\"address\":\"1 rue du Test\",\"purchasePrice\":1,\"legalProfile\":{\"estimatedAnnualEnergyCost\":0},\"privateAreas\":[]}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("validation_failed"))
|
||||
.andExpect(
|
||||
jsonPath(
|
||||
"$.errors[*].field",
|
||||
containsInAnyOrder("legalProfile.referenceYearPresentWhenEnergyCostIsProvided")))
|
||||
.andExpect(jsonPath("$.errors[*].code", containsInAnyOrder("assert_true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void apartmentRequestsExposeEveryInvalidPrivateAreaWithIndexedStableFieldsAndCodes()
|
||||
throws Exception {
|
||||
|
||||
+41
@@ -94,6 +94,47 @@ void legalProfileRejectsNegativeAndStructurallyInvalidValues() {
|
||||
() -> ApartmentLegalProfile.ConstructionPeriod.fromCode("1900_2000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legalProfileAcceptsFrenchDepartmentCodesAndDropsAnOrphanedEnergyReferenceYear() {
|
||||
assertEquals(true, ApartmentLegalProfile.isValidDepartmentCode("01"));
|
||||
assertEquals(true, ApartmentLegalProfile.isValidDepartmentCode("2a"));
|
||||
assertEquals(true, ApartmentLegalProfile.isValidDepartmentCode("95"));
|
||||
assertEquals(true, ApartmentLegalProfile.isValidDepartmentCode("971"));
|
||||
assertEquals(true, ApartmentLegalProfile.isValidDepartmentCode("976"));
|
||||
assertEquals(false, ApartmentLegalProfile.isValidDepartmentCode("20"));
|
||||
assertEquals(false, ApartmentLegalProfile.isValidDepartmentCode("96"));
|
||||
assertEquals(false, ApartmentLegalProfile.isValidDepartmentCode("977"));
|
||||
|
||||
ApartmentLegalProfile profile =
|
||||
new ApartmentLegalProfile(
|
||||
"",
|
||||
null,
|
||||
"",
|
||||
null,
|
||||
BigDecimal.ZERO,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
BigDecimal.ZERO,
|
||||
1900,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"");
|
||||
|
||||
assertEquals(BigDecimal.ZERO, profile.habitableSurface());
|
||||
assertEquals(BigDecimal.ZERO, profile.estimatedAnnualEnergyCost());
|
||||
assertEquals(1900, profile.estimatedAnnualEnergyCostReferenceYear());
|
||||
assertEquals(
|
||||
null,
|
||||
new ApartmentLegalProfile(
|
||||
"", null, "", null, null, "", "", "", "", "", null, 1800, "", "", "", "", "")
|
||||
.estimatedAnnualEnergyCostReferenceYear());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsOneCurrentDocumentPerType() {
|
||||
Apartment apartment = apartment();
|
||||
|
||||
Reference in new issue
Block a user