Refactor backend services and controllers to use DTOs for Tenant, Lease, and Apartment
- Updated TenantController to utilize TenantService and DTOs for tenant operations. - Refactored UserController to return UserResponse DTO. - Introduced LeaseService and ApartmentService to handle business logic and data mapping. - Created DTOs for Apartment, Lease, Tenant, and User to standardize data transfer. - Implemented mappers for converting between entities and DTOs using MapStruct. - Removed landlordId from Apartment and Lease entities and adjusted related tests. - Added validation and normalization logic for tenant and guarantor data. - Enhanced unit tests to ensure proper mapping and data integrity during updates.
This commit is contained in:
1 parent
181410e28f
commit
c8020c489e
34 files changed
+757
-321
No files matched your search
@@ -28,6 +28,7 @@
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -59,6 +60,11 @@
|
||||
<artifactId>openhtmltopdf-pdfbox</artifactId>
|
||||
<version>1.1.40</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -83,6 +89,13 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<!-- Deprecated APIs are forbidden in production and test code. -->
|
||||
<compilerArgs>
|
||||
<arg>-Xlint:deprecation</arg>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package fr.nessar.lmnporganizer.backend;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.service.GreetingService;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -7,16 +8,14 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
private final GreetingService greetingService;
|
||||
|
||||
public HelloController(GreetingService greetingService) {
|
||||
this.greetingService = greetingService;
|
||||
}
|
||||
|
||||
@GetMapping("/api/hello")
|
||||
public String hello(@AuthenticationPrincipal OAuth2User user) {
|
||||
String name = user.getAttribute("preferred_username");
|
||||
if (name == null) {
|
||||
name = user.getAttribute("name");
|
||||
}
|
||||
if (name == null) {
|
||||
name = user.getName();
|
||||
}
|
||||
return "Hello, " + name + "!";
|
||||
return greetingService.greetingFor(user);
|
||||
}
|
||||
}
|
||||
+28
-49
@@ -1,8 +1,8 @@
|
||||
package fr.nessar.lmnporganizer.backend.controller;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.ApartmentService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -11,56 +11,35 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/apartments")
|
||||
public class ApartmentController {
|
||||
private final ApartmentService apartmentService;
|
||||
|
||||
private final ApartmentRepository apartmentRepository;
|
||||
public ApartmentController(ApartmentService apartmentService) {
|
||||
this.apartmentService = apartmentService;
|
||||
}
|
||||
|
||||
public ApartmentController(ApartmentRepository apartmentRepository) {
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
}
|
||||
@GetMapping
|
||||
public List<ApartmentResponse> getAllApartments() {
|
||||
return apartmentService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Apartment> getAllApartments() {
|
||||
return apartmentRepository.findAll();
|
||||
}
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApartmentResponse> getApartmentById(@PathVariable String id) {
|
||||
return apartmentService.getById(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Apartment> getApartmentById(@PathVariable String id) {
|
||||
return apartmentRepository.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
@PostMapping
|
||||
public ResponseEntity<ApartmentResponse> createApartment(@RequestBody ApartmentRequest request) {
|
||||
return apartmentService.create(request);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Apartment> createApartment(@RequestBody Apartment apartment) {
|
||||
apartment.setId(null); // Ensure a new entity is created
|
||||
apartment.setBailleurId("default-landlord"); // Associate with default landlord profile
|
||||
Apartment savedApartment = apartmentRepository.save(apartment);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(savedApartment);
|
||||
}
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApartmentResponse> updateApartment(@PathVariable String id,
|
||||
@RequestBody ApartmentRequest request) {
|
||||
return apartmentService.update(id, request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Apartment> updateApartment(@PathVariable String id, @RequestBody Apartment apartmentDetails) {
|
||||
return apartmentRepository.findById(id)
|
||||
.map(existingApartment -> {
|
||||
existingApartment.setName(apartmentDetails.getName());
|
||||
existingApartment.setAddress(apartmentDetails.getAddress());
|
||||
existingApartment.setPurchasePrice(apartmentDetails.getPurchasePrice());
|
||||
existingApartment.setDescription(apartmentDetails.getDescription());
|
||||
// Keep bailleurId fixed to default-landlord in single-portfolio mode
|
||||
existingApartment.setBailleurId("default-landlord");
|
||||
Apartment updatedApartment = apartmentRepository.save(existingApartment);
|
||||
return ResponseEntity.ok(updatedApartment);
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteApartment(@PathVariable String id) {
|
||||
if (apartmentRepository.existsById(id)) {
|
||||
apartmentRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteApartment(@PathVariable String id) {
|
||||
return apartmentService.delete(id);
|
||||
}
|
||||
}
|
||||
+25
-25
@@ -11,34 +11,34 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class ApiExceptionHandler {
|
||||
@ExceptionHandler(AttachmentTooLargeException.class)
|
||||
public ResponseEntity<ApiError> handleAttachmentTooLarge(AttachmentTooLargeException exception) {
|
||||
return error(HttpStatus.CONTENT_TOO_LARGE, exception.getMessage());
|
||||
}
|
||||
@ExceptionHandler(AttachmentTooLargeException.class)
|
||||
public ResponseEntity<ApiError> handleAttachmentTooLarge(AttachmentTooLargeException exception) {
|
||||
return error(HttpStatus.CONTENT_TOO_LARGE, exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<ApiError> handleMultipartTooLarge() {
|
||||
return error(HttpStatus.CONTENT_TOO_LARGE, "Une pièce jointe ne peut pas dépasser 250 Mo.");
|
||||
}
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<ApiError> handleMultipartTooLarge() {
|
||||
return error(HttpStatus.CONTENT_TOO_LARGE, "Une pièce jointe ne peut pas dépasser 250 Mo.");
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidPhoneNumberException.class)
|
||||
public ResponseEntity<ApiError> handleInvalidPhone(InvalidPhoneNumberException exception) {
|
||||
return error(HttpStatus.BAD_REQUEST, exception.getMessage());
|
||||
}
|
||||
@ExceptionHandler(InvalidPhoneNumberException.class)
|
||||
public ResponseEntity<ApiError> handleInvalidPhone(InvalidPhoneNumberException exception) {
|
||||
return error(HttpStatus.BAD_REQUEST, exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiError> handleInvalidRequest(MethodArgumentNotValidException exception) {
|
||||
String message = exception.getBindingResult().getFieldErrors().stream()
|
||||
.findFirst()
|
||||
.map(error -> error.getDefaultMessage())
|
||||
.orElse("La requête est invalide.");
|
||||
return error(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiError> handleInvalidRequest(MethodArgumentNotValidException exception) {
|
||||
String message = exception.getBindingResult().getFieldErrors().stream()
|
||||
.findFirst()
|
||||
.map(error -> error.getDefaultMessage())
|
||||
.orElse("La requête est invalide.");
|
||||
return error(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
private ResponseEntity<ApiError> error(HttpStatus status, String message) {
|
||||
return ResponseEntity.status(status).body(new ApiError(message));
|
||||
}
|
||||
private ResponseEntity<ApiError> error(HttpStatus status, String message) {
|
||||
return ResponseEntity.status(status).body(new ApiError(message));
|
||||
}
|
||||
|
||||
private record ApiError(String message) {
|
||||
}
|
||||
private record ApiError(String message) {
|
||||
}
|
||||
}
|
||||
+32
-113
@@ -1,132 +1,51 @@
|
||||
package fr.nessar.lmnporganizer.backend.controller;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
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.TenantRepository;
|
||||
import fr.nessar.lmnporganizer.backend.service.LeasePdfService;
|
||||
import fr.nessar.lmnporganizer.backend.service.PhoneNumberNormalizer;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.LeaseService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/leases")
|
||||
public class LeaseController {
|
||||
private final LeaseRepository leaseRepository;
|
||||
private final ApartmentRepository apartmentRepository;
|
||||
private final TenantRepository tenantRepository;
|
||||
private final LeasePdfService leasePdfService;
|
||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||
private final LeaseService leaseService;
|
||||
|
||||
public LeaseController(LeaseRepository leaseRepository, ApartmentRepository apartmentRepository,
|
||||
TenantRepository tenantRepository, LeasePdfService leasePdfService,
|
||||
PhoneNumberNormalizer phoneNumberNormalizer) {
|
||||
this.leaseRepository = leaseRepository;
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.leasePdfService = leasePdfService;
|
||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||
}
|
||||
public LeaseController(LeaseService leaseService) {
|
||||
this.leaseService = leaseService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Lease> getAllLeases() {
|
||||
return leaseRepository.findAll();
|
||||
}
|
||||
@GetMapping
|
||||
public List<LeaseResponse> getAllLeases() {
|
||||
return leaseService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Lease> getLease(@PathVariable String id) {
|
||||
return leaseRepository.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<LeaseResponse> getLease(@PathVariable String id) {
|
||||
return leaseService.getById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Lease> createLease(@Valid @RequestBody Lease lease) {
|
||||
if (!referencesExist(lease)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
lease.setId(null);
|
||||
lease.setLandlordId("default-landlord");
|
||||
normalizeGuarantorPhones(lease);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(leaseRepository.save(lease));
|
||||
}
|
||||
@PostMapping
|
||||
public ResponseEntity<LeaseResponse> createLease(@Valid @RequestBody LeaseRequest request) {
|
||||
return leaseService.create(request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Lease> updateLease(@PathVariable String id, @Valid @RequestBody Lease details) {
|
||||
if (!referencesExist(details)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return leaseRepository.findById(id).map(lease -> {
|
||||
copyTerms(details, lease);
|
||||
return ResponseEntity.ok(leaseRepository.save(lease));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<LeaseResponse> updateLease(@PathVariable String id,
|
||||
@Valid @RequestBody LeaseRequest request) {
|
||||
return leaseService.update(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteLease(@PathVariable String id) {
|
||||
if (!leaseRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
leaseRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteLease(@PathVariable String id) {
|
||||
return leaseService.delete(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/pdf")
|
||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id) {
|
||||
Lease lease = leaseRepository.findById(id).orElse(null);
|
||||
if (lease == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Apartment apartment = apartmentRepository.findById(lease.getApartmentId()).orElse(null);
|
||||
List<Tenant> tenants = tenantsFor(lease.getTenantIds());
|
||||
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
private boolean referencesExist(Lease lease) {
|
||||
if (lease.getApartmentId() == null || !apartmentRepository.existsById(lease.getApartmentId())
|
||||
|| lease.getTenantIds().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return tenantsFor(lease.getTenantIds()).size() == new HashSet<>(lease.getTenantIds()).size();
|
||||
}
|
||||
|
||||
private List<Tenant> tenantsFor(List<String> ids) {
|
||||
List<Tenant> tenants = new ArrayList<>();
|
||||
tenantRepository.findAllById(ids).forEach(tenants::add);
|
||||
return tenants;
|
||||
}
|
||||
|
||||
private void copyTerms(Lease source, Lease target) {
|
||||
target.setLandlordId("default-landlord");
|
||||
target.setApartmentId(source.getApartmentId());
|
||||
target.setTenantIds(source.getTenantIds());
|
||||
target.setGuarantors(source.getGuarantors());
|
||||
normalizeGuarantorPhones(target);
|
||||
target.setStartDate(source.getStartDate());
|
||||
target.setEndDate(source.getEndDate());
|
||||
target.setMonthlyRent(source.getMonthlyRent());
|
||||
target.setMonthlyCharges(source.getMonthlyCharges());
|
||||
target.setSecurityDeposit(source.getSecurityDeposit());
|
||||
target.setStatus(source.getStatus());
|
||||
}
|
||||
|
||||
private void normalizeGuarantorPhones(Lease lease) {
|
||||
lease.getGuarantors().forEach(guarantor -> guarantor.setPhone(phoneNumberNormalizer.normalize(guarantor.getPhone())));
|
||||
}
|
||||
@GetMapping("/{id}/pdf")
|
||||
public ResponseEntity<byte[]> downloadPdf(@PathVariable String id) {
|
||||
return leaseService.downloadPdf(id);
|
||||
}
|
||||
}
|
||||
+44
-106
@@ -1,16 +1,11 @@
|
||||
package fr.nessar.lmnporganizer.backend.controller;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.model.TenantAttachment;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import fr.nessar.lmnporganizer.backend.service.TenantAttachmentStorage;
|
||||
import fr.nessar.lmnporganizer.backend.service.AttachmentUploadPolicy;
|
||||
import fr.nessar.lmnporganizer.backend.service.PhoneNumberNormalizer;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantAttachmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.TenantService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -22,110 +17,53 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/tenants")
|
||||
public class TenantController {
|
||||
private final TenantRepository tenantRepository;
|
||||
private final TenantAttachmentStorage attachmentStorage;
|
||||
private final AttachmentUploadPolicy attachmentUploadPolicy;
|
||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||
private final TenantService tenantService;
|
||||
|
||||
public TenantController(TenantRepository tenantRepository, TenantAttachmentStorage attachmentStorage,
|
||||
AttachmentUploadPolicy attachmentUploadPolicy, PhoneNumberNormalizer phoneNumberNormalizer) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.attachmentStorage = attachmentStorage;
|
||||
this.attachmentUploadPolicy = attachmentUploadPolicy;
|
||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||
}
|
||||
public TenantController(TenantService tenantService) {
|
||||
this.tenantService = tenantService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Tenant> getAllTenants() {
|
||||
return tenantRepository.findAll();
|
||||
}
|
||||
@GetMapping
|
||||
public List<TenantResponse> getAllTenants() {
|
||||
return tenantService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Tenant> getTenant(@PathVariable String id) {
|
||||
return tenantRepository.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<TenantResponse> getTenant(@PathVariable String id) {
|
||||
return tenantService.getById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Tenant> createTenant(@Valid @RequestBody Tenant tenant) {
|
||||
tenant.setId(null);
|
||||
tenant.setLandlordId("default-landlord");
|
||||
tenant.setPhone(phoneNumberNormalizer.normalize(tenant.getPhone()));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(tenantRepository.save(tenant));
|
||||
}
|
||||
@PostMapping
|
||||
public ResponseEntity<TenantResponse> createTenant(@Valid @RequestBody TenantRequest request) {
|
||||
return tenantService.create(request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Tenant> updateTenant(@PathVariable String id, @Valid @RequestBody Tenant details) {
|
||||
return tenantRepository.findById(id).map(tenant -> {
|
||||
tenant.setFirstName(details.getFirstName());
|
||||
tenant.setLastName(details.getLastName());
|
||||
tenant.setEmail(details.getEmail());
|
||||
tenant.setPhone(phoneNumberNormalizer.normalize(details.getPhone()));
|
||||
tenant.setAddress(details.getAddress());
|
||||
tenant.setLandlordId("default-landlord");
|
||||
return ResponseEntity.ok(tenantRepository.save(tenant));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<TenantResponse> updateTenant(@PathVariable String id,
|
||||
@Valid @RequestBody TenantRequest request) {
|
||||
return tenantService.update(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteTenant(@PathVariable String id) {
|
||||
if (!tenantRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
tenantRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteTenant(@PathVariable String id) {
|
||||
return tenantService.delete(id);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/{tenantId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<TenantAttachment> uploadAttachment(@PathVariable String tenantId,
|
||||
@RequestPart("file") MultipartFile file) throws IOException {
|
||||
Tenant tenant = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenant == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
attachmentUploadPolicy.validate(file);
|
||||
TenantAttachment attachment = attachmentStorage.store(tenantId, file);
|
||||
tenant.getAttachments().add(attachment);
|
||||
tenantRepository.save(tenant);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(attachment);
|
||||
}
|
||||
@PostMapping(path = "/{tenantId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<TenantAttachmentResponse> uploadAttachment(@PathVariable String tenantId,
|
||||
@RequestPart("file") MultipartFile file) throws IOException {
|
||||
return tenantService.uploadAttachment(tenantId, file);
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Resource> downloadAttachment(@PathVariable String tenantId, @PathVariable String attachmentId)
|
||||
throws IOException {
|
||||
TenantAttachment attachment = tenantRepository.findById(tenantId)
|
||||
.flatMap(tenant -> tenant.getAttachments().stream().filter(item -> item.getId().equals(attachmentId)).findFirst())
|
||||
.orElse(null);
|
||||
if (attachment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
MediaType mediaType;
|
||||
try {
|
||||
mediaType = MediaType.parseMediaType(attachment.getContentType());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.contentType(mediaType)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.attachment().filename(attachment.getFileName()).build().toString())
|
||||
.body(attachmentStorage.read(attachment));
|
||||
}
|
||||
@GetMapping("/{tenantId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Resource> downloadAttachment(@PathVariable String tenantId, @PathVariable String attachmentId)
|
||||
throws IOException {
|
||||
return tenantService.downloadAttachment(tenantId, attachmentId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{tenantId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Void> deleteAttachment(@PathVariable String tenantId, @PathVariable String attachmentId)
|
||||
throws IOException {
|
||||
Tenant tenant = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenant == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
TenantAttachment attachment = tenant.getAttachments().stream()
|
||||
.filter(item -> item.getId().equals(attachmentId)).findFirst().orElse(null);
|
||||
if (attachment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
attachmentStorage.delete(attachment);
|
||||
tenant.getAttachments().remove(attachment);
|
||||
tenantRepository.save(tenant);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@DeleteMapping("/{tenantId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Void> deleteAttachment(@PathVariable String tenantId, @PathVariable String attachmentId)
|
||||
throws IOException {
|
||||
return tenantService.deleteAttachment(tenantId, attachmentId);
|
||||
}
|
||||
}
|
||||
+11
-13
@@ -1,26 +1,24 @@
|
||||
package fr.nessar.lmnporganizer.backend.controller;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.UserResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.UserService;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping("/user")
|
||||
public Map<String, Object> getUser(@AuthenticationPrincipal OidcUser principal) {
|
||||
if (principal == null) {
|
||||
return Map.of("authenticated", false);
|
||||
}
|
||||
return Map.of(
|
||||
"authenticated", true,
|
||||
"username", principal.getPreferredUsername() != null ? principal.getPreferredUsername() : principal.getName(),
|
||||
"email", principal.getEmail() != null ? principal.getEmail() : ""
|
||||
);
|
||||
}
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
public UserResponse getUser(@AuthenticationPrincipal OidcUser principal) {
|
||||
return userService.getUser(principal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
public record ApartmentRequest(String name, String address, double purchasePrice, String description) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
public record ApartmentResponse(String id, String name, String address, double purchasePrice, String description) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record GuarantorDto(
|
||||
String id,
|
||||
@NotBlank(message = "Le prénom du garant est obligatoire.") String firstName,
|
||||
@NotBlank(message = "Le nom du garant est obligatoire.") String lastName,
|
||||
@NotBlank(message = "L'adresse e-mail du garant est obligatoire.") @Email(message = "L'adresse e-mail du garant est invalide.") String email,
|
||||
String phone,
|
||||
String address
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.LeaseStatus;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record LeaseRequest(
|
||||
String apartmentId,
|
||||
List<String> tenantIds,
|
||||
List<@Valid GuarantorDto> guarantors,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
BigDecimal monthlyRent,
|
||||
BigDecimal monthlyCharges,
|
||||
BigDecimal securityDeposit,
|
||||
LeaseStatus status
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.LeaseStatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record LeaseResponse(
|
||||
String id,
|
||||
String apartmentId,
|
||||
List<String> tenantIds,
|
||||
List<GuarantorDto> guarantors,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
BigDecimal monthlyRent,
|
||||
BigDecimal monthlyCharges,
|
||||
BigDecimal securityDeposit,
|
||||
LeaseStatus status
|
||||
) {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
public record TenantAttachmentResponse(String id, String fileName, String contentType, long size) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record TenantRequest(
|
||||
@NotBlank(message = "Le prénom est obligatoire.") String firstName,
|
||||
@NotBlank(message = "Le nom est obligatoire.") String lastName,
|
||||
@NotBlank(message = "L'adresse e-mail est obligatoire.") @Email(message = "L'adresse e-mail est invalide.") String email,
|
||||
String phone,
|
||||
String address
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TenantResponse(
|
||||
String id,
|
||||
String firstName,
|
||||
String lastName,
|
||||
String email,
|
||||
String phone,
|
||||
String address,
|
||||
List<TenantAttachmentResponse> attachments
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
public record UserResponse(boolean authenticated, String username, String email) {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package fr.nessar.lmnporganizer.backend.mapper;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingConstants;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR)
|
||||
public interface ApartmentMapper {
|
||||
|
||||
ApartmentResponse toResponse(Apartment apartment);
|
||||
|
||||
List<ApartmentResponse> toResponses(List<Apartment> apartments);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "bailleurId", ignore = true)
|
||||
Apartment toEntity(ApartmentRequest request);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "bailleurId", ignore = true)
|
||||
void update(ApartmentRequest request, @MappingTarget Apartment target);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.nessar.lmnporganizer.backend.mapper;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.GuarantorDto;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseResponse;
|
||||
import fr.nessar.lmnporganizer.backend.model.Guarantor;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingConstants;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR)
|
||||
public interface LeaseMapper {
|
||||
|
||||
LeaseResponse toResponse(Lease lease);
|
||||
|
||||
List<LeaseResponse> toResponses(List<Lease> leases);
|
||||
|
||||
GuarantorDto toDto(Guarantor guarantor);
|
||||
|
||||
Guarantor toEntity(GuarantorDto guarantor);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "landlordId", ignore = true)
|
||||
Lease toEntity(LeaseRequest request);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "landlordId", ignore = true)
|
||||
void update(LeaseRequest request, @MappingTarget Lease target);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.nessar.lmnporganizer.backend.mapper;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantAttachmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantResponse;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.model.TenantAttachment;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingConstants;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.ERROR)
|
||||
public interface TenantMapper {
|
||||
|
||||
TenantResponse toResponse(Tenant tenant);
|
||||
|
||||
List<TenantResponse> toResponses(List<Tenant> tenants);
|
||||
|
||||
TenantAttachmentResponse toResponse(TenantAttachment attachment);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "landlordId", ignore = true)
|
||||
@Mapping(target = "attachments", ignore = true)
|
||||
Tenant toEntity(TenantRequest request);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "landlordId", ignore = true)
|
||||
@Mapping(target = "attachments", ignore = true)
|
||||
void update(TenantRequest request, @MappingTarget Tenant target);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package fr.nessar.lmnporganizer.backend.repository;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface LeaseRepository extends MongoRepository<Lease, String> {
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package fr.nessar.lmnporganizer.backend.repository;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TenantRepository extends MongoRepository<Tenant, String> {
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package fr.nessar.lmnporganizer.backend.service;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.mapper.ApartmentMapper;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ApartmentService {
|
||||
private final ApartmentRepository apartmentRepository;
|
||||
private final ApartmentMapper apartmentMapper;
|
||||
|
||||
public ApartmentService(ApartmentRepository apartmentRepository, ApartmentMapper apartmentMapper) {
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
this.apartmentMapper = apartmentMapper;
|
||||
}
|
||||
|
||||
public List<ApartmentResponse> getAll() {
|
||||
return apartmentMapper.toResponses(apartmentRepository.findAll());
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> getById(String id) {
|
||||
return apartmentRepository.findById(id).map(apartmentMapper::toResponse).map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> create(ApartmentRequest request) {
|
||||
Apartment apartment = apartmentMapper.toEntity(request);
|
||||
apartment.setBailleurId("default-landlord");
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(apartmentMapper.toResponse(apartmentRepository.save(apartment)));
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> update(String id, ApartmentRequest request) {
|
||||
return apartmentRepository.findById(id).map(apartment -> {
|
||||
apartmentMapper.update(request, apartment);
|
||||
apartment.setBailleurId("default-landlord");
|
||||
return ResponseEntity.ok(apartmentMapper.toResponse(apartmentRepository.save(apartment)));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<Void> delete(String id) {
|
||||
if (!apartmentRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
apartmentRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package fr.nessar.lmnporganizer.backend.service;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class GreetingService {
|
||||
public String greetingFor(OAuth2User user) {
|
||||
String name = user.getAttribute("preferred_username");
|
||||
if (name == null) {
|
||||
name = user.getAttribute("name");
|
||||
}
|
||||
return "Hello, " + (name == null ? user.getName() : name) + "!";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package fr.nessar.lmnporganizer.backend.service;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseRequest;
|
||||
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.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LeaseRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class LeaseService {
|
||||
private final LeaseRepository leaseRepository;
|
||||
private final ApartmentRepository apartmentRepository;
|
||||
private final TenantRepository tenantRepository;
|
||||
private final LeasePdfService leasePdfService;
|
||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||
private final LeaseMapper leaseMapper;
|
||||
|
||||
public LeaseService(LeaseRepository leaseRepository, ApartmentRepository apartmentRepository,
|
||||
TenantRepository tenantRepository, LeasePdfService leasePdfService,
|
||||
PhoneNumberNormalizer phoneNumberNormalizer, LeaseMapper leaseMapper) {
|
||||
this.leaseRepository = leaseRepository;
|
||||
this.apartmentRepository = apartmentRepository;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.leasePdfService = leasePdfService;
|
||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||
this.leaseMapper = leaseMapper;
|
||||
}
|
||||
|
||||
public List<LeaseResponse> getAll() {
|
||||
return leaseMapper.toResponses(leaseRepository.findAll());
|
||||
}
|
||||
|
||||
public ResponseEntity<LeaseResponse> getById(String id) {
|
||||
return leaseRepository.findById(id).map(leaseMapper::toResponse).map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<LeaseResponse> create(LeaseRequest request) {
|
||||
Lease lease = leaseMapper.toEntity(request);
|
||||
if (!referencesExist(lease)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
lease.setLandlordId("default-landlord");
|
||||
normalizeGuarantorPhones(lease);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(leaseMapper.toResponse(leaseRepository.save(lease)));
|
||||
}
|
||||
|
||||
public ResponseEntity<LeaseResponse> update(String id, LeaseRequest request) {
|
||||
Lease details = leaseMapper.toEntity(request);
|
||||
if (!referencesExist(details)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return leaseRepository.findById(id).map(lease -> {
|
||||
leaseMapper.update(request, lease);
|
||||
lease.setLandlordId("default-landlord");
|
||||
normalizeGuarantorPhones(lease);
|
||||
return ResponseEntity.ok(leaseMapper.toResponse(leaseRepository.save(lease)));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<Void> delete(String id) {
|
||||
if (!leaseRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
leaseRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> downloadPdf(String id) {
|
||||
Lease lease = leaseRepository.findById(id).orElse(null);
|
||||
if (lease == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Apartment apartment = apartmentRepository.findById(lease.getApartmentId()).orElse(null);
|
||||
List<Tenant> tenants = tenantsFor(lease.getTenantIds());
|
||||
if (apartment == null || tenants.size() != new HashSet<>(lease.getTenantIds()).size()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
private boolean referencesExist(Lease lease) {
|
||||
if (lease.getApartmentId() == null || !apartmentRepository.existsById(lease.getApartmentId())
|
||||
|| lease.getTenantIds().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return tenantsFor(lease.getTenantIds()).size() == new HashSet<>(lease.getTenantIds()).size();
|
||||
}
|
||||
|
||||
private List<Tenant> tenantsFor(List<String> ids) {
|
||||
List<Tenant> tenants = new ArrayList<>();
|
||||
tenantRepository.findAllById(ids).forEach(tenants::add);
|
||||
return tenants;
|
||||
}
|
||||
|
||||
private void normalizeGuarantorPhones(Lease lease) {
|
||||
lease.getGuarantors().forEach(guarantor -> guarantor.setPhone(phoneNumberNormalizer.normalize(guarantor.getPhone())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package fr.nessar.lmnporganizer.backend.service;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantAttachmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantResponse;
|
||||
import fr.nessar.lmnporganizer.backend.mapper.TenantMapper;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.model.TenantAttachment;
|
||||
import fr.nessar.lmnporganizer.backend.repository.TenantRepository;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class TenantService {
|
||||
private final TenantRepository tenantRepository;
|
||||
private final TenantAttachmentStorage attachmentStorage;
|
||||
private final AttachmentUploadPolicy attachmentUploadPolicy;
|
||||
private final PhoneNumberNormalizer phoneNumberNormalizer;
|
||||
private final TenantMapper tenantMapper;
|
||||
|
||||
public TenantService(TenantRepository tenantRepository, TenantAttachmentStorage attachmentStorage,
|
||||
AttachmentUploadPolicy attachmentUploadPolicy, PhoneNumberNormalizer phoneNumberNormalizer,
|
||||
TenantMapper tenantMapper) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.attachmentStorage = attachmentStorage;
|
||||
this.attachmentUploadPolicy = attachmentUploadPolicy;
|
||||
this.phoneNumberNormalizer = phoneNumberNormalizer;
|
||||
this.tenantMapper = tenantMapper;
|
||||
}
|
||||
|
||||
public List<TenantResponse> getAll() {
|
||||
return tenantMapper.toResponses(tenantRepository.findAll());
|
||||
}
|
||||
|
||||
public ResponseEntity<TenantResponse> getById(String id) {
|
||||
return tenantRepository.findById(id).map(tenantMapper::toResponse).map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<TenantResponse> create(TenantRequest request) {
|
||||
Tenant tenant = tenantMapper.toEntity(request);
|
||||
tenant.setLandlordId("default-landlord");
|
||||
tenant.setPhone(phoneNumberNormalizer.normalize(tenant.getPhone()));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(tenantMapper.toResponse(tenantRepository.save(tenant)));
|
||||
}
|
||||
|
||||
public ResponseEntity<TenantResponse> update(String id, TenantRequest request) {
|
||||
return tenantRepository.findById(id).map(tenant -> {
|
||||
tenantMapper.update(request, tenant);
|
||||
tenant.setPhone(phoneNumberNormalizer.normalize(request.phone()));
|
||||
tenant.setLandlordId("default-landlord");
|
||||
return ResponseEntity.ok(tenantMapper.toResponse(tenantRepository.save(tenant)));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
public ResponseEntity<Void> delete(String id) {
|
||||
if (!tenantRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
tenantRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
public ResponseEntity<TenantAttachmentResponse> uploadAttachment(String tenantId, MultipartFile file) throws IOException {
|
||||
Tenant tenant = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenant == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
attachmentUploadPolicy.validate(file);
|
||||
TenantAttachment attachment = attachmentStorage.store(tenantId, file);
|
||||
tenant.getAttachments().add(attachment);
|
||||
tenantRepository.save(tenant);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(tenantMapper.toResponse(attachment));
|
||||
}
|
||||
|
||||
public ResponseEntity<Resource> downloadAttachment(String tenantId, String attachmentId) throws IOException {
|
||||
TenantAttachment attachment = tenantRepository.findById(tenantId)
|
||||
.flatMap(tenant -> tenant.getAttachments().stream().filter(item -> item.getId().equals(attachmentId)).findFirst())
|
||||
.orElse(null);
|
||||
if (attachment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.contentType(mediaTypeFor(attachment))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.attachment().filename(attachment.getFileName()).build().toString())
|
||||
.body(attachmentStorage.read(attachment));
|
||||
}
|
||||
|
||||
public ResponseEntity<Void> deleteAttachment(String tenantId, String attachmentId) throws IOException {
|
||||
Tenant tenant = tenantRepository.findById(tenantId).orElse(null);
|
||||
if (tenant == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
TenantAttachment attachment = tenant.getAttachments().stream()
|
||||
.filter(item -> item.getId().equals(attachmentId)).findFirst().orElse(null);
|
||||
if (attachment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
attachmentStorage.delete(attachment);
|
||||
tenant.getAttachments().remove(attachment);
|
||||
tenantRepository.save(tenant);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private MediaType mediaTypeFor(TenantAttachment attachment) {
|
||||
try {
|
||||
return MediaType.parseMediaType(attachment.getContentType());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.nessar.lmnporganizer.backend.service;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.UserResponse;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
public UserResponse getUser(OidcUser principal) {
|
||||
if (principal == null) {
|
||||
return new UserResponse(false, null, null);
|
||||
}
|
||||
String username = principal.getPreferredUsername() != null ? principal.getPreferredUsername() : principal.getName();
|
||||
String email = principal.getEmail() != null ? principal.getEmail() : "";
|
||||
return new UserResponse(true, username, email);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ class ApartmentControllerTest {
|
||||
"{\"name\":\"New Apartment\",\"address\":\"Address\",\"purchasePrice\":200000.0,\"description\":\"Desc\"}"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value("1"))
|
||||
.andExpect(jsonPath("$.bailleurId").value("default-landlord"));
|
||||
.andExpect(jsonPath("$.bailleurId").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ class LeaseControllerTest {
|
||||
.content(leaseJson()))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value("lease-1"))
|
||||
.andExpect(jsonPath("$.landlordId").value("default-landlord"))
|
||||
.andExpect(jsonPath("$.landlordId").doesNotExist())
|
||||
.andExpect(jsonPath("$.apartmentId").value("apartment-1"))
|
||||
.andExpect(jsonPath("$.tenantIds[0]").value("tenant-1"))
|
||||
.andExpect(jsonPath("$.guarantors[0].lastName").value("Durand"))
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class TenantControllerTest {
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value("tenant-1"))
|
||||
.andExpect(jsonPath("$.landlordId").value("default-landlord"));
|
||||
.andExpect(jsonPath("$.landlordId").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package fr.nessar.lmnporganizer.backend.mapper;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.LeaseRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.TenantRequest;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Lease;
|
||||
import fr.nessar.lmnporganizer.backend.model.LeaseStatus;
|
||||
import fr.nessar.lmnporganizer.backend.model.Tenant;
|
||||
import fr.nessar.lmnporganizer.backend.model.TenantAttachment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class UpdateMapperTest {
|
||||
@Test
|
||||
void preservesApartmentIdentityWhenUpdatingItsDetails() {
|
||||
Apartment target = new Apartment("apartment-1", "default-landlord", "Old", "Old", 1, "Old");
|
||||
ApartmentRequest source = new ApartmentRequest("New", "New", 2, "New");
|
||||
|
||||
Mappers.getMapper(ApartmentMapper.class).update(source, target);
|
||||
|
||||
assertEquals("apartment-1", target.getId());
|
||||
assertEquals("default-landlord", target.getBailleurId());
|
||||
assertEquals("New", target.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesTenantOwnershipAndAttachmentsWhenUpdatingContactDetails() {
|
||||
Tenant target = new Tenant("tenant-1", "default-landlord", "Old", "Name", "old@example.com", "", "");
|
||||
target.setAttachments(List.of(new TenantAttachment("attachment-1", "id.pdf", "application/pdf", 1, "id.pdf")));
|
||||
TenantRequest source = new TenantRequest("New", "Name", "new@example.com", "", "");
|
||||
|
||||
Mappers.getMapper(TenantMapper.class).update(source, target);
|
||||
|
||||
assertEquals("tenant-1", target.getId());
|
||||
assertEquals("default-landlord", target.getLandlordId());
|
||||
assertEquals(1, target.getAttachments().size());
|
||||
assertEquals("new@example.com", target.getEmail());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesLeaseIdentityAndOwnershipWhenUpdatingTerms() {
|
||||
Lease target = new Lease("lease-1", "default-landlord", "old-apartment", List.of(), List.of(),
|
||||
LocalDate.of(2026, 1, 1), LocalDate.of(2027, 1, 1), BigDecimal.ONE, BigDecimal.ZERO,
|
||||
BigDecimal.ONE, LeaseStatus.DRAFT);
|
||||
LeaseRequest source = new LeaseRequest("new-apartment", List.of("tenant-1"), List.of(),
|
||||
LocalDate.of(2026, 2, 1), LocalDate.of(2027, 2, 1), BigDecimal.TEN, BigDecimal.ONE,
|
||||
BigDecimal.TEN, LeaseStatus.ACTIVE);
|
||||
|
||||
Mappers.getMapper(LeaseMapper.class).update(source, target);
|
||||
|
||||
assertEquals("lease-1", target.getId());
|
||||
assertEquals("default-landlord", target.getLandlordId());
|
||||
assertEquals("new-apartment", target.getApartmentId());
|
||||
assertEquals(LeaseStatus.ACTIVE, target.getStatus());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user