feat(#23): use landlord profiles in PDFs
This commit is contained in:
1 parent
ac5a7942c9
commit
668680e844
17 files changed
+221
-39
No files matched your search
+6
@@ -3,6 +3,7 @@ package fr.nessar.lmnporganizer.backend.controller;
|
||||
import fr.nessar.lmnporganizer.backend.exception.AttachmentTooLargeException;
|
||||
import fr.nessar.lmnporganizer.backend.exception.EmailAlreadyUsedException;
|
||||
import fr.nessar.lmnporganizer.backend.exception.InvalidPhoneNumberException;
|
||||
import fr.nessar.lmnporganizer.backend.exception.IncompleteLandlordProfileException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
@@ -32,6 +33,11 @@ public class ApiExceptionHandler {
|
||||
return error(HttpStatus.CONFLICT, exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IncompleteLandlordProfileException.class)
|
||||
public ResponseEntity<ApiError> handleIncompleteLandlordProfile(IncompleteLandlordProfileException exception) {
|
||||
return error(HttpStatus.UNPROCESSABLE_CONTENT, exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiError> handleInvalidRequest(MethodArgumentNotValidException exception) {
|
||||
String message = exception.getBindingResult().getFieldErrors().stream()
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@ import fr.nessar.lmnporganizer.backend.dto.LeaseResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.LeaseService;
|
||||
import jakarta.validation.Valid;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -45,7 +47,7 @@ public class LeaseController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/pdf")
|
||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id) {
|
||||
return leaseService.downloadPdf(id);
|
||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id, @AuthenticationPrincipal OidcUser principal) {
|
||||
return leaseService.downloadPdf(id, principal.getSubject());
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -3,6 +3,8 @@ package fr.nessar.lmnporganizer.backend.controller;
|
||||
import fr.nessar.lmnporganizer.backend.dto.RentPaymentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.RentPaymentService;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -33,7 +35,7 @@ public class RentPaymentController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/receipt")
|
||||
public ResponseEntity<byte[]> downloadReceipt(@PathVariable String id) {
|
||||
return rentPaymentService.downloadReceipt(id);
|
||||
public ResponseEntity<byte[]> downloadReceipt(@PathVariable String id, @AuthenticationPrincipal OidcUser principal) {
|
||||
return rentPaymentService.downloadReceipt(id, principal.getSubject());
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package fr.nessar.lmnporganizer.backend.exception;
|
||||
|
||||
public class IncompleteLandlordProfileException extends RuntimeException {
|
||||
public IncompleteLandlordProfileException() {
|
||||
super("Votre profil bailleur est incomplet. Complétez-le dans les réglages avant de générer un PDF.");
|
||||
}
|
||||
}
|
||||
+19
@@ -3,6 +3,7 @@ package fr.nessar.lmnporganizer.backend.service;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LandlordProfileRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LandlordProfileResponse;
|
||||
import fr.nessar.lmnporganizer.backend.exception.EmailAlreadyUsedException;
|
||||
import fr.nessar.lmnporganizer.backend.exception.IncompleteLandlordProfileException;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
@@ -43,6 +44,15 @@ public class LandlordProfileService {
|
||||
return response(save(landlord));
|
||||
}
|
||||
|
||||
public Landlord requireCompleteProfileFor(String oidcSubject) {
|
||||
Landlord landlord = landlordRepository.findByOidcSubject(oidcSubject)
|
||||
.orElseThrow(IncompleteLandlordProfileException::new);
|
||||
if (isIncomplete(landlord)) {
|
||||
throw new IncompleteLandlordProfileException();
|
||||
}
|
||||
return landlord;
|
||||
}
|
||||
|
||||
private Landlord save(Landlord landlord) {
|
||||
try {
|
||||
return landlordRepository.save(landlord);
|
||||
@@ -64,4 +74,13 @@ public class LandlordProfileService {
|
||||
private String value(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private boolean isIncomplete(Landlord landlord) {
|
||||
return isBlank(landlord.getFirstName()) || isBlank(landlord.getLastName()) || isBlank(landlord.getEmail())
|
||||
|| isBlank(landlord.getPhone()) || isBlank(landlord.getAddress());
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package fr.nessar.lmnporganizer.backend.service;
|
||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
@@ -21,10 +22,11 @@ public class LeasePdfService {
|
||||
this.templateEngine = templateEngine;
|
||||
}
|
||||
|
||||
public byte[] generate(Lease lease, Apartment apartment, List<Tenant> tenants) {
|
||||
public byte[] generate(Lease lease, Apartment apartment, Landlord landlord, List<Tenant> tenants) {
|
||||
Context context = new Context(Locale.FRANCE);
|
||||
context.setVariable("lease", lease);
|
||||
context.setVariable("apartment", apartment);
|
||||
context.setVariable("landlord", landlord);
|
||||
context.setVariable("tenantNames", tenantNamesOf(tenants));
|
||||
context.setVariable("guarantorNames", namesOf(lease.getGuarantors().stream()
|
||||
.map(guarantor -> guarantor.getFirstName() + " " + guarantor.getLastName()).toList()));
|
||||
|
||||
@@ -5,6 +5,7 @@ import fr.nessar.lmnporganizer.backend.dto.LeaseResponse;
|
||||
import fr.nessar.lmnporganizer.backend.mapper.LeaseMapper;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
@@ -29,11 +30,12 @@ public class LeaseService {
|
||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||
private final LeaseMapper leaseMapper;
|
||||
private final RentPaymentService rentPaymentService;
|
||||
private final LandlordProfileService landlordProfileService;
|
||||
|
||||
public LeaseService(LeaseRepository leaseRepository, ApartmentRepository apartmentRepository,
|
||||
TenantRepository tenantRepository, LeasePdfService leasePdfService,
|
||||
PhoneNumberNormalizer phoneNumberNormalizer, LeaseMapper leaseMapper,
|
||||
RentPaymentService rentPaymentService) {
|
||||
RentPaymentService rentPaymentService, LandlordProfileService landlordProfileService) {
|
||||
this.leaseRepository = leaseRepository;
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
this.tenantRepository = tenantRepository;
|
||||
@@ -41,6 +43,7 @@ public class LeaseService {
|
||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||
this.leaseMapper = leaseMapper;
|
||||
this.rentPaymentService = rentPaymentService;
|
||||
this.landlordProfileService = landlordProfileService;
|
||||
}
|
||||
|
||||
public List<LeaseResponse> getAll() {
|
||||
@@ -92,7 +95,7 @@ public class LeaseService {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> downloadPdf(String id) {
|
||||
public ResponseEntity<byte[]> downloadPdf(String id, String oidcSubject) {
|
||||
Lease lease = leaseRepository.findById(id).orElse(null);
|
||||
if (lease == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -102,11 +105,12 @@ public class LeaseService {
|
||||
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Landlord landlord = landlordProfileService.requireCompleteProfileFor(oidcSubject);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.attachment().filename("bail-" + id + ".pdf").build().toString())
|
||||
.body(leasePdfService.generate(lease, apartment, tenants));
|
||||
.body(leasePdfService.generate(lease, apartment, landlord, tenants));
|
||||
}
|
||||
|
||||
private boolean referencesExist(Lease lease) {
|
||||
|
||||
+6
-7
@@ -8,7 +8,6 @@ import fr.nessar.lmnporganizer.backend.model.RentPayment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
@@ -31,19 +30,19 @@ public class RentPaymentService {
|
||||
private final RentPaymentRepository rentPaymentRepository;
|
||||
private final LeaseRepository leaseRepository;
|
||||
private final ApartmentRepository apartmentRepository;
|
||||
private final LandlordRepository landlordRepository;
|
||||
private final LandlordProfileService landlordProfileService;
|
||||
private final TenantRepository tenantRepository;
|
||||
private final RentReceiptPdfService rentReceiptPdfService;
|
||||
private final Clock clock;
|
||||
|
||||
public RentPaymentService(RentPaymentRepository rentPaymentRepository, LeaseRepository leaseRepository,
|
||||
ApartmentRepository apartmentRepository, LandlordRepository landlordRepository,
|
||||
ApartmentRepository apartmentRepository, LandlordProfileService landlordProfileService,
|
||||
TenantRepository tenantRepository,
|
||||
RentReceiptPdfService rentReceiptPdfService, Clock clock) {
|
||||
this.rentPaymentRepository = rentPaymentRepository;
|
||||
this.leaseRepository = leaseRepository;
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
this.landlordRepository = landlordRepository;
|
||||
this.landlordProfileService = landlordProfileService;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.rentReceiptPdfService = rentReceiptPdfService;
|
||||
this.clock = clock;
|
||||
@@ -65,7 +64,7 @@ public class RentPaymentService {
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> downloadReceipt(String id) {
|
||||
public ResponseEntity<byte[]> downloadReceipt(String id, String oidcSubject) {
|
||||
RentPayment payment = rentPaymentRepository.findById(id).orElse(null);
|
||||
if (payment == null)
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -76,9 +75,9 @@ public class RentPaymentService {
|
||||
if (lease == null)
|
||||
return ResponseEntity.notFound().build();
|
||||
Apartment apartment = apartmentRepository.findById(lease.getApartmentId()).orElse(null);
|
||||
Landlord landlord = landlordRepository.findById(payment.getLandlordId()).orElse(null);
|
||||
Landlord landlord = landlordProfileService.requireCompleteProfileFor(oidcSubject);
|
||||
List<Tenant> tenants = tenantsFor(lease.getTenantIds());
|
||||
if (apartment == null || landlord == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<table>
|
||||
<tr><th>Logement</th><td th:text="${apartment.name}">Studio République</td></tr>
|
||||
<tr><th>Adresse</th><td th:text="${apartment.address}">12 rue de la République</td></tr>
|
||||
<tr><th>Bailleur</th><td th:text="${landlord.firstName + ' ' + landlord.lastName}">Léa Dubois</td></tr>
|
||||
<tr><th>E-mail du bailleur</th><td th:text="${landlord.email}">lea@example.com</td></tr>
|
||||
<tr><th>Téléphone du bailleur</th><td th:text="${landlord.phone}">+33612345678</td></tr>
|
||||
<tr><th>Adresse du bailleur</th><td th:text="${landlord.address}">1 rue de Paris</td></tr>
|
||||
<tr><th>Locataire(s)</th><td th:text="${tenantNames}">Camille Martin</td></tr>
|
||||
<tr><th>Période</th><td><span th:text="${lease.startDate}">2026-09-01</span> au <span th:text="${lease.endDate}">2027-08-31</span></td></tr>
|
||||
<tr><th>Loyer mensuel</th><td><span th:text="${lease.monthlyRent}">850.00</span> EUR</td></tr>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<p>Le bailleur reconnaît avoir reçu le paiement du loyer et des charges pour la période indiquée.</p>
|
||||
<table>
|
||||
<tr><th>Bailleur</th><td th:text="${landlord.firstName + ' ' + landlord.lastName}">Bailleur LMNP</td></tr>
|
||||
<tr><th>E-mail du bailleur</th><td th:text="${landlord.email}">bailleur@example.com</td></tr>
|
||||
<tr><th>Téléphone du bailleur</th><td th:text="${landlord.phone}">0102030405</td></tr>
|
||||
<tr><th>Adresse du bailleur</th><td th:text="${landlord.address}">123 rue de la Gestion, Paris</td></tr>
|
||||
<tr><th>Logement</th><td th:text="${apartment.name}">Studio République</td></tr>
|
||||
<tr><th>Adresse</th><td th:text="${apartment.address}">12 rue de la République</td></tr>
|
||||
|
||||
+67
-2
@@ -4,8 +4,10 @@ import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Guarantor;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import fr.nessar.lmnporganizer.backend.model.LeaseStatus;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import fr.nessar.lmnporganizer.backend.service.RentPaymentService;
|
||||
@@ -27,6 +29,7 @@ import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
@@ -46,6 +49,9 @@ class LeaseControllerTest {
|
||||
@MockitoBean
|
||||
private TenantRepository tenantRepository;
|
||||
|
||||
@MockitoBean
|
||||
private LandlordRepository landlordRepository;
|
||||
|
||||
@MockitoBean
|
||||
private RentPaymentService rentPaymentService;
|
||||
|
||||
@@ -142,8 +148,10 @@ class LeaseControllerTest {
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment));
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.of(landlord()));
|
||||
|
||||
mockMvc.perform(get("/api/leases/lease-1/pdf").with(oidcLogin()))
|
||||
mockMvc.perform(get("/api/leases/lease-1/pdf")
|
||||
.with(oidcLogin().idToken(token -> token.claim("sub", "subject-1"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Type", "application/pdf"))
|
||||
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("bail-lease-1.pdf")))
|
||||
@@ -154,7 +162,9 @@ class LeaseControllerTest {
|
||||
}
|
||||
try (PDDocument document = Loader.loadPDF(content)) {
|
||||
String pdfText = new PDFTextStripper().getText(document);
|
||||
if (!pdfText.contains("Studio République") || !pdfText.contains("Camille Martin")) {
|
||||
if (!pdfText.contains("Studio République") || !pdfText.contains("Camille Martin")
|
||||
|| !pdfText.contains("Léa Dubois") || !pdfText.contains("lea@example.com")
|
||||
|| !pdfText.contains("+33612345678") || !pdfText.contains("1 rue de Paris")) {
|
||||
throw new AssertionError("Expected rendered lease template values");
|
||||
}
|
||||
} catch (java.io.IOException exception) {
|
||||
@@ -163,11 +173,66 @@ class LeaseControllerTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenLandlordProfileIsIncomplete_thenLeasePdfDownloadIsRejectedWithSettingsGuidance() throws Exception {
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(new Apartment("apartment-1",
|
||||
"default-landlord", "Studio République", "12 rue de la République", 150000, "")));
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.of(new Landlord("profile-1",
|
||||
"subject-1", "Léa", "Dubois", "lea@example.com", "+33612345678", "")));
|
||||
|
||||
mockMvc.perform(get("/api/leases/lease-1/pdf")
|
||||
.with(oidcLogin().idToken(token -> token.claim("sub", "subject-1"))))
|
||||
.andExpect(status().isUnprocessableContent())
|
||||
.andExpect(jsonPath("$.message").value(
|
||||
"Votre profil bailleur est incomplet. Complétez-le dans les réglages avant de générer un PDF."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenLandlordProfileChanges_thenTheNextLeasePdfUsesItsUpdatedContactDetails() throws Exception {
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(new Apartment("apartment-1",
|
||||
"default-landlord", "Studio République", "12 rue de la République", 150000, "")));
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
Landlord updatedLandlord = new Landlord("profile-1", "subject-1", "Alice", "Renard",
|
||||
"alice@example.com", "+33698765432", "2 rue de Lyon");
|
||||
when(landlordRepository.findByOidcSubject("subject-1"))
|
||||
.thenReturn(Optional.of(landlord()))
|
||||
.thenReturn(Optional.of(updatedLandlord));
|
||||
|
||||
String firstPdfText = downloadedLeasePdfText();
|
||||
String updatedPdfText = downloadedLeasePdfText();
|
||||
|
||||
assertTrue(firstPdfText.contains("Léa Dubois"));
|
||||
assertTrue(updatedPdfText.contains("Alice Renard"));
|
||||
assertTrue(updatedPdfText.contains("alice@example.com"));
|
||||
assertTrue(updatedPdfText.contains("+33698765432"));
|
||||
assertTrue(updatedPdfText.contains("2 rue de Lyon"));
|
||||
}
|
||||
|
||||
private Tenant tenant() {
|
||||
return new Tenant("tenant-1", "default-landlord", "Camille", "Martin", "camille@example.com",
|
||||
"+33612345678", "10 rue des Lilas");
|
||||
}
|
||||
|
||||
private Landlord landlord() {
|
||||
return new Landlord("profile-1", "subject-1", "Léa", "Dubois", "lea@example.com", "+33612345678",
|
||||
"1 rue de Paris");
|
||||
}
|
||||
|
||||
private String downloadedLeasePdfText() throws Exception {
|
||||
byte[] content = mockMvc.perform(get("/api/leases/lease-1/pdf")
|
||||
.with(oidcLogin().idToken(token -> token.claim("sub", "subject-1"))))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsByteArray();
|
||||
try (PDDocument document = Loader.loadPDF(content)) {
|
||||
return new PDFTextStripper().getText(document);
|
||||
}
|
||||
}
|
||||
|
||||
private Lease lease() {
|
||||
return new Lease("lease-1", "default-landlord", "apartment-1", List.of("tenant-1"),
|
||||
List.of(new Guarantor("guarantor-1", "Alex", "Durand", "alex@example.com", "+33600000000",
|
||||
|
||||
+36
-3
@@ -11,6 +11,9 @@ import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -104,13 +107,43 @@ class RentPaymentControllerTest {
|
||||
when(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment(LocalDate.of(2026, 7, 2))));
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment()));
|
||||
when(landlordRepository.findById("default-landlord")).thenReturn(Optional.of(landlord()));
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.of(landlord()));
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
|
||||
mockMvc.perform(get("/api/rent-payments/payment-1/receipt").with(oidcLogin()))
|
||||
mockMvc.perform(get("/api/rent-payments/payment-1/receipt")
|
||||
.with(oidcLogin().idToken(token -> token.claim("sub", "subject-1"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Type", "application/pdf"))
|
||||
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("quittance-2026-07.pdf")));
|
||||
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("quittance-2026-07.pdf")))
|
||||
.andExpect(result -> {
|
||||
try (PDDocument document = Loader.loadPDF(result.getResponse().getContentAsByteArray())) {
|
||||
String pdfText = new PDFTextStripper().getText(document);
|
||||
if (!pdfText.contains("Bailleur LMNP") || !pdfText.contains("bailleur@example.com")
|
||||
|| !pdfText.contains("0102030405")
|
||||
|| !pdfText.contains("123 rue de la Gestion, Paris")) {
|
||||
throw new AssertionError("Expected rendered landlord profile values");
|
||||
}
|
||||
} catch (java.io.IOException exception) {
|
||||
throw new AssertionError("Expected a readable PDF", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenLandlordProfileIsIncomplete_thenReceiptDownloadIsRejectedWithSettingsGuidance() throws Exception {
|
||||
when(rentPaymentRepository.findById("payment-1"))
|
||||
.thenReturn(Optional.of(payment(LocalDate.of(2026, 7, 2))));
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment()));
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.of(new Landlord("profile-1",
|
||||
"subject-1", "Bailleur", "LMNP", "bailleur@example.com", "0102030405", "")));
|
||||
|
||||
mockMvc.perform(get("/api/rent-payments/payment-1/receipt")
|
||||
.with(oidcLogin().idToken(token -> token.claim("sub", "subject-1"))))
|
||||
.andExpect(status().isUnprocessableContent())
|
||||
.andExpect(jsonPath("$.message").value(
|
||||
"Votre profil bailleur est incomplet. Complétez-le dans les réglages avant de générer un PDF."));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+5
-6
@@ -10,7 +10,6 @@ import fr.nessar.lmnporganizer.backend.model.RentPaymentStatus;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import org.apache.pdfbox.Loader;
|
||||
@@ -51,7 +50,7 @@ class RentPaymentServiceTest {
|
||||
private ApartmentRepository apartmentRepository;
|
||||
|
||||
@Mock
|
||||
private LandlordRepository landlordRepository;
|
||||
private LandlordProfileService landlordProfileService;
|
||||
|
||||
@Mock
|
||||
private TenantRepository tenantRepository;
|
||||
@@ -66,7 +65,7 @@ class RentPaymentServiceTest {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.setTemplateResolver(resolver);
|
||||
service = new RentPaymentService(rentPaymentRepository, leaseRepository, apartmentRepository,
|
||||
landlordRepository, tenantRepository, new RentReceiptPdfService(templateEngine),
|
||||
landlordProfileService, tenantRepository, new RentReceiptPdfService(templateEngine),
|
||||
Clock.fixed(Instant.parse("2026-07-17T00:00:00Z"), ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
@@ -119,10 +118,10 @@ class RentPaymentServiceTest {
|
||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease(
|
||||
LocalDate.of(2026, 1, 1), LocalDate.of(2026, 12, 31))));
|
||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment()));
|
||||
when(landlordRepository.findById("default-landlord")).thenReturn(Optional.of(landlord()));
|
||||
when(landlordProfileService.requireCompleteProfileFor("subject-1")).thenReturn(landlord());
|
||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
||||
|
||||
ResponseEntity<byte[]> response = service.downloadReceipt("payment-1");
|
||||
ResponseEntity<byte[]> response = service.downloadReceipt("payment-1", "subject-1");
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("application/pdf", response.getHeaders().getContentType().toString());
|
||||
@@ -137,7 +136,7 @@ class RentPaymentServiceTest {
|
||||
|
||||
RentPayment unpaid = payment("payment-2", "2026-08", LocalDate.of(2026, 8, 1), null);
|
||||
when(rentPaymentRepository.findById("payment-2")).thenReturn(Optional.of(unpaid));
|
||||
assertEquals(HttpStatus.CONFLICT, service.downloadReceipt("payment-2").getStatusCode());
|
||||
assertEquals(HttpStatus.CONFLICT, service.downloadReceipt("payment-2", "subject-1").getStatusCode());
|
||||
}
|
||||
|
||||
private Apartment apartment() {
|
||||
|
||||
Reference in new issue
Block a user