feat(#23): use landlord profiles in PDFs

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent ac5a7942c9
commit 668680e844
17 files changed
+221 -39

No files matched your search

@@ -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()
@@ -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());
}
}
@@ -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());
}
}
@@ -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.");
}
}
@@ -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) {
@@ -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>
@@ -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",
@@ -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
@@ -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() {