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.AttachmentTooLargeException;
|
||||||
import fr.nessar.lmnporganizer.backend.exception.EmailAlreadyUsedException;
|
import fr.nessar.lmnporganizer.backend.exception.EmailAlreadyUsedException;
|
||||||
import fr.nessar.lmnporganizer.backend.exception.InvalidPhoneNumberException;
|
import fr.nessar.lmnporganizer.backend.exception.InvalidPhoneNumberException;
|
||||||
|
import fr.nessar.lmnporganizer.backend.exception.IncompleteLandlordProfileException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
@@ -32,6 +33,11 @@ public class ApiExceptionHandler {
|
|||||||
return error(HttpStatus.CONFLICT, exception.getMessage());
|
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)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiError> handleInvalidRequest(MethodArgumentNotValidException exception) {
|
public ResponseEntity<ApiError> handleInvalidRequest(MethodArgumentNotValidException exception) {
|
||||||
String message = exception.getBindingResult().getFieldErrors().stream()
|
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 fr.nessar.lmnporganizer.backend.service.LeaseService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.ResponseEntity;
|
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 org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -45,7 +47,7 @@ public class LeaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/pdf")
|
@GetMapping("/{id}/pdf")
|
||||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id) {
|
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id, @AuthenticationPrincipal OidcUser principal) {
|
||||||
return leaseService.downloadPdf(id);
|
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.dto.RentPaymentResponse;
|
||||||
import fr.nessar.lmnporganizer.backend.service.RentPaymentService;
|
import fr.nessar.lmnporganizer.backend.service.RentPaymentService;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
@@ -33,7 +35,7 @@ public class RentPaymentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/receipt")
|
@GetMapping("/{id}/receipt")
|
||||||
public ResponseEntity<byte[]> downloadReceipt(@PathVariable String id) {
|
public ResponseEntity<byte[]> downloadReceipt(@PathVariable String id, @AuthenticationPrincipal OidcUser principal) {
|
||||||
return rentPaymentService.downloadReceipt(id);
|
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.LandlordProfileRequest;
|
||||||
import fr.nessar.lmnporganizer.backend.dto.LandlordProfileResponse;
|
import fr.nessar.lmnporganizer.backend.dto.LandlordProfileResponse;
|
||||||
import fr.nessar.lmnporganizer.backend.exception.EmailAlreadyUsedException;
|
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.model.Landlord;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
@@ -43,6 +44,15 @@ public class LandlordProfileService {
|
|||||||
return response(save(landlord));
|
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) {
|
private Landlord save(Landlord landlord) {
|
||||||
try {
|
try {
|
||||||
return landlordRepository.save(landlord);
|
return landlordRepository.save(landlord);
|
||||||
@@ -64,4 +74,13 @@ public class LandlordProfileService {
|
|||||||
private String value(String value) {
|
private String value(String value) {
|
||||||
return value == null ? "" : 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 com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
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.model.Tenant;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.thymeleaf.TemplateEngine;
|
import org.thymeleaf.TemplateEngine;
|
||||||
@@ -21,10 +22,11 @@ public class LeasePdfService {
|
|||||||
this.templateEngine = templateEngine;
|
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 context = new Context(Locale.FRANCE);
|
||||||
context.setVariable("lease", lease);
|
context.setVariable("lease", lease);
|
||||||
context.setVariable("apartment", apartment);
|
context.setVariable("apartment", apartment);
|
||||||
|
context.setVariable("landlord", landlord);
|
||||||
context.setVariable("tenantNames", tenantNamesOf(tenants));
|
context.setVariable("tenantNames", tenantNamesOf(tenants));
|
||||||
context.setVariable("guarantorNames", namesOf(lease.getGuarantors().stream()
|
context.setVariable("guarantorNames", namesOf(lease.getGuarantors().stream()
|
||||||
.map(guarantor -> guarantor.getFirstName() + " " + guarantor.getLastName()).toList()));
|
.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.mapper.LeaseMapper;
|
||||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
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.model.Tenant;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||||
@@ -29,11 +30,12 @@ public class LeaseService {
|
|||||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||||
private final LeaseMapper leaseMapper;
|
private final LeaseMapper leaseMapper;
|
||||||
private final RentPaymentService rentPaymentService;
|
private final RentPaymentService rentPaymentService;
|
||||||
|
private final LandlordProfileService landlordProfileService;
|
||||||
|
|
||||||
public LeaseService(LeaseRepository leaseRepository, ApartmentRepository apartmentRepository,
|
public LeaseService(LeaseRepository leaseRepository, ApartmentRepository apartmentRepository,
|
||||||
TenantRepository tenantRepository, LeasePdfService leasePdfService,
|
TenantRepository tenantRepository, LeasePdfService leasePdfService,
|
||||||
PhoneNumberNormalizer phoneNumberNormalizer, LeaseMapper leaseMapper,
|
PhoneNumberNormalizer phoneNumberNormalizer, LeaseMapper leaseMapper,
|
||||||
RentPaymentService rentPaymentService) {
|
RentPaymentService rentPaymentService, LandlordProfileService landlordProfileService) {
|
||||||
this.leaseRepository = leaseRepository;
|
this.leaseRepository = leaseRepository;
|
||||||
this.apartmentRepository = apartmentRepository;
|
this.apartmentRepository = apartmentRepository;
|
||||||
this.tenantRepository = tenantRepository;
|
this.tenantRepository = tenantRepository;
|
||||||
@@ -41,6 +43,7 @@ public class LeaseService {
|
|||||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||||
this.leaseMapper = leaseMapper;
|
this.leaseMapper = leaseMapper;
|
||||||
this.rentPaymentService = rentPaymentService;
|
this.rentPaymentService = rentPaymentService;
|
||||||
|
this.landlordProfileService = landlordProfileService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<LeaseResponse> getAll() {
|
public List<LeaseResponse> getAll() {
|
||||||
@@ -92,7 +95,7 @@ public class LeaseService {
|
|||||||
return ResponseEntity.noContent().build();
|
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);
|
Lease lease = leaseRepository.findById(id).orElse(null);
|
||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -102,11 +105,12 @@ public class LeaseService {
|
|||||||
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
Landlord landlord = landlordProfileService.requireCompleteProfileFor(oidcSubject);
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
ContentDisposition.attachment().filename("bail-" + id + ".pdf").build().toString())
|
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) {
|
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.model.Tenant;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
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.RentPaymentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
@@ -31,19 +30,19 @@ public class RentPaymentService {
|
|||||||
private final RentPaymentRepository rentPaymentRepository;
|
private final RentPaymentRepository rentPaymentRepository;
|
||||||
private final LeaseRepository leaseRepository;
|
private final LeaseRepository leaseRepository;
|
||||||
private final ApartmentRepository apartmentRepository;
|
private final ApartmentRepository apartmentRepository;
|
||||||
private final LandlordRepository landlordRepository;
|
private final LandlordProfileService landlordProfileService;
|
||||||
private final TenantRepository tenantRepository;
|
private final TenantRepository tenantRepository;
|
||||||
private final RentReceiptPdfService rentReceiptPdfService;
|
private final RentReceiptPdfService rentReceiptPdfService;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
public RentPaymentService(RentPaymentRepository rentPaymentRepository, LeaseRepository leaseRepository,
|
public RentPaymentService(RentPaymentRepository rentPaymentRepository, LeaseRepository leaseRepository,
|
||||||
ApartmentRepository apartmentRepository, LandlordRepository landlordRepository,
|
ApartmentRepository apartmentRepository, LandlordProfileService landlordProfileService,
|
||||||
TenantRepository tenantRepository,
|
TenantRepository tenantRepository,
|
||||||
RentReceiptPdfService rentReceiptPdfService, Clock clock) {
|
RentReceiptPdfService rentReceiptPdfService, Clock clock) {
|
||||||
this.rentPaymentRepository = rentPaymentRepository;
|
this.rentPaymentRepository = rentPaymentRepository;
|
||||||
this.leaseRepository = leaseRepository;
|
this.leaseRepository = leaseRepository;
|
||||||
this.apartmentRepository = apartmentRepository;
|
this.apartmentRepository = apartmentRepository;
|
||||||
this.landlordRepository = landlordRepository;
|
this.landlordProfileService = landlordProfileService;
|
||||||
this.tenantRepository = tenantRepository;
|
this.tenantRepository = tenantRepository;
|
||||||
this.rentReceiptPdfService = rentReceiptPdfService;
|
this.rentReceiptPdfService = rentReceiptPdfService;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
@@ -65,7 +64,7 @@ public class RentPaymentService {
|
|||||||
}).orElse(ResponseEntity.notFound().build());
|
}).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);
|
RentPayment payment = rentPaymentRepository.findById(id).orElse(null);
|
||||||
if (payment == null)
|
if (payment == null)
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
@@ -76,9 +75,9 @@ public class RentPaymentService {
|
|||||||
if (lease == null)
|
if (lease == null)
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
Apartment apartment = apartmentRepository.findById(lease.getApartmentId()).orElse(null);
|
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());
|
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();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr><th>Logement</th><td th:text="${apartment.name}">Studio République</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>
|
<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>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>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>
|
<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>
|
<p>Le bailleur reconnaît avoir reçu le paiement du loyer et des charges pour la période indiquée.</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Bailleur</th><td th:text="${landlord.firstName + ' ' + landlord.lastName}">Bailleur LMNP</td></tr>
|
<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>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>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>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.Guarantor;
|
||||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||||
import fr.nessar.lmnporganizer.backend.model.LeaseStatus;
|
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.model.Tenant;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
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.LeaseRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.service.RentPaymentService;
|
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.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.*;
|
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.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
@@ -46,6 +49,9 @@ class LeaseControllerTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
private TenantRepository tenantRepository;
|
private TenantRepository tenantRepository;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private LandlordRepository landlordRepository;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
private RentPaymentService rentPaymentService;
|
private RentPaymentService rentPaymentService;
|
||||||
|
|
||||||
@@ -142,8 +148,10 @@ class LeaseControllerTest {
|
|||||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease));
|
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease));
|
||||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment));
|
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment));
|
||||||
when(tenantRepository.findAllById(List.of("tenant-1"))).thenReturn(List.of(tenant()));
|
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(status().isOk())
|
||||||
.andExpect(header().string("Content-Type", "application/pdf"))
|
.andExpect(header().string("Content-Type", "application/pdf"))
|
||||||
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("bail-lease-1.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)) {
|
try (PDDocument document = Loader.loadPDF(content)) {
|
||||||
String pdfText = new PDFTextStripper().getText(document);
|
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");
|
throw new AssertionError("Expected rendered lease template values");
|
||||||
}
|
}
|
||||||
} catch (java.io.IOException exception) {
|
} 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() {
|
private Tenant tenant() {
|
||||||
return new Tenant("tenant-1", "default-landlord", "Camille", "Martin", "camille@example.com",
|
return new Tenant("tenant-1", "default-landlord", "Camille", "Martin", "camille@example.com",
|
||||||
"+33612345678", "10 rue des Lilas");
|
"+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() {
|
private Lease lease() {
|
||||||
return new Lease("lease-1", "default-landlord", "apartment-1", List.of("tenant-1"),
|
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",
|
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.LeaseRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository;
|
import fr.nessar.lmnporganizer.backend.repository.RentPaymentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
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.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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(rentPaymentRepository.findById("payment-1")).thenReturn(Optional.of(payment(LocalDate.of(2026, 7, 2))));
|
||||||
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease()));
|
||||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment()));
|
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()));
|
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(status().isOk())
|
||||||
.andExpect(header().string("Content-Type", "application/pdf"))
|
.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
|
@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.model.Tenant;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
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.RentPaymentRepository;
|
||||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
@@ -51,7 +50,7 @@ class RentPaymentServiceTest {
|
|||||||
private ApartmentRepository apartmentRepository;
|
private ApartmentRepository apartmentRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LandlordRepository landlordRepository;
|
private LandlordProfileService landlordProfileService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private TenantRepository tenantRepository;
|
private TenantRepository tenantRepository;
|
||||||
@@ -66,7 +65,7 @@ class RentPaymentServiceTest {
|
|||||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
templateEngine.setTemplateResolver(resolver);
|
templateEngine.setTemplateResolver(resolver);
|
||||||
service = new RentPaymentService(rentPaymentRepository, leaseRepository, apartmentRepository,
|
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));
|
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(
|
when(leaseRepository.findById("lease-1")).thenReturn(Optional.of(lease(
|
||||||
LocalDate.of(2026, 1, 1), LocalDate.of(2026, 12, 31))));
|
LocalDate.of(2026, 1, 1), LocalDate.of(2026, 12, 31))));
|
||||||
when(apartmentRepository.findById("apartment-1")).thenReturn(Optional.of(apartment()));
|
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()));
|
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(HttpStatus.OK, response.getStatusCode());
|
||||||
assertEquals("application/pdf", response.getHeaders().getContentType().toString());
|
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);
|
RentPayment unpaid = payment("payment-2", "2026-08", LocalDate.of(2026, 8, 1), null);
|
||||||
when(rentPaymentRepository.findById("payment-2")).thenReturn(Optional.of(unpaid));
|
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() {
|
private Apartment apartment() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
import { finalize, Observable } from 'rxjs';
|
import { finalize, Observable } from 'rxjs';
|
||||||
import { withoutRecordKey, withoutSetValue } from './immutable-state';
|
import { withoutRecordKey, withoutSetValue } from './immutable-state';
|
||||||
|
import { httpErrorMessage } from './http-error-message';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class FileDownloadService {
|
export class FileDownloadService {
|
||||||
@@ -23,10 +24,29 @@ export class FileDownloadService {
|
|||||||
.pipe(finalize(() => this.pendingKeys.update((keys) => withoutSetValue(keys, key))))
|
.pipe(finalize(() => this.pendingKeys.update((keys) => withoutSetValue(keys, key))))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (blob) => this.save(blob, fileName),
|
next: (blob) => this.save(blob, fileName),
|
||||||
error: () => this.errors.update((errors) => ({ ...errors, [key]: errorMessage })),
|
error: (error) => this.recordError(key, error, errorMessage),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private recordError(key: string, error: unknown, fallback: string): void {
|
||||||
|
if (this.hasBlobBody(error)) {
|
||||||
|
void error.error
|
||||||
|
.text()
|
||||||
|
.then((body) => this.setError(key, httpErrorMessage({ error: JSON.parse(body) }, fallback)))
|
||||||
|
.catch(() => this.setError(key, fallback));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setError(key, httpErrorMessage(error, fallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasBlobBody(error: unknown): error is { error: Blob } {
|
||||||
|
return typeof error === 'object' && error !== null && 'error' in error && error.error instanceof Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setError(key: string, message: string): void {
|
||||||
|
this.errors.update((errors) => ({ ...errors, [key]: message }));
|
||||||
|
}
|
||||||
|
|
||||||
private save(blob: Blob, fileName: string): void {
|
private save(blob: Blob, fileName: string): void {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function httpErrorMessage(error: unknown, fallback: string): string {
|
||||||
|
return typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'error' in error &&
|
||||||
|
typeof error.error === 'object' &&
|
||||||
|
error.error !== null &&
|
||||||
|
'message' in error.error &&
|
||||||
|
typeof error.error.message === 'string'
|
||||||
|
? error.error.message
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { finalize } from 'rxjs';
|
|||||||
import { FileDownloadService } from './file-download.service';
|
import { FileDownloadService } from './file-download.service';
|
||||||
import { TenantAttachment, TenantService } from './tenant.service';
|
import { TenantAttachment, TenantService } from './tenant.service';
|
||||||
import { withoutRecordKey, withoutSetValue } from './immutable-state';
|
import { withoutRecordKey, withoutSetValue } from './immutable-state';
|
||||||
|
import { httpErrorMessage } from './http-error-message';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TenantAttachmentTransferService {
|
export class TenantAttachmentTransferService {
|
||||||
@@ -54,7 +55,7 @@ export class TenantAttachmentTransferService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: unknown) => {
|
error: (error: unknown) => {
|
||||||
const message = this.errorMessage(error, 'Impossible d’envoyer cette pièce jointe.');
|
const message = httpErrorMessage(error, 'Impossible d’envoyer cette pièce jointe.');
|
||||||
this.errors.update((errors) => ({ ...errors, [tenantId]: message }));
|
this.errors.update((errors) => ({ ...errors, [tenantId]: message }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -98,15 +99,4 @@ export class TenantAttachmentTransferService {
|
|||||||
return `attachment:${attachmentId}`;
|
return `attachment:${attachmentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private errorMessage(error: unknown, fallback: string): string {
|
|
||||||
return typeof error === 'object' &&
|
|
||||||
error !== null &&
|
|
||||||
'error' in error &&
|
|
||||||
typeof error.error === 'object' &&
|
|
||||||
error.error !== null &&
|
|
||||||
'message' in error.error &&
|
|
||||||
typeof error.error.message === 'string'
|
|
||||||
? error.error.message
|
|
||||||
: fallback;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -415,4 +415,21 @@ describe('WorkspaceService', () => {
|
|||||||
'Impossible d’envoyer cette pièce jointe.',
|
'Impossible d’envoyer cette pièce jointe.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps profile-completion guidance returned as a blocked PDF download blob', async () => {
|
||||||
|
const service = TestBed.inject(WorkspaceService);
|
||||||
|
leases.downloadPdf.mockReturnValueOnce(
|
||||||
|
throwError(() => ({
|
||||||
|
error: new Blob(['{"message":"Complétez votre profil dans les réglages."}'], {
|
||||||
|
type: 'application/json',
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
service.downloadLeasePdf(lease);
|
||||||
|
|
||||||
|
await vi.waitFor(() =>
|
||||||
|
expect(service.leaseDownloadError(lease.id)).toBe('Complétez votre profil dans les réglages.'),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user