diff --git a/backend/pom.xml b/backend/pom.xml index ed63a83..b288d8f 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -28,6 +28,7 @@ 25 + 1.6.3 @@ -59,6 +60,11 @@ openhtmltopdf-pdfbox 1.1.40 + + org.mapstruct + mapstruct + ${mapstruct.version} + org.springframework.boot @@ -83,6 +89,13 @@ org.apache.maven.plugins maven-compiler-plugin + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + -Xlint:deprecation diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java index f0caa0c..8394346 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java @@ -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); } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApartmentController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApartmentController.java index 425d38f..1eaa425 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApartmentController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApartmentController.java @@ -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 getAllApartments() { + return apartmentService.getAll(); + } - @GetMapping - public List getAllApartments() { - return apartmentRepository.findAll(); - } + @GetMapping("/{id}") + public ResponseEntity getApartmentById(@PathVariable String id) { + return apartmentService.getById(id); + } - @GetMapping("/{id}") - public ResponseEntity getApartmentById(@PathVariable String id) { - return apartmentRepository.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } + @PostMapping + public ResponseEntity createApartment(@RequestBody ApartmentRequest request) { + return apartmentService.create(request); + } - @PostMapping - public ResponseEntity 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 updateApartment(@PathVariable String id, + @RequestBody ApartmentRequest request) { + return apartmentService.update(id, request); + } - @PutMapping("/{id}") - public ResponseEntity 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 deleteApartment(@PathVariable String id) { - if (apartmentRepository.existsById(id)) { - apartmentRepository.deleteById(id); - return ResponseEntity.noContent().build(); - } else { - return ResponseEntity.notFound().build(); - } - } + @DeleteMapping("/{id}") + public ResponseEntity deleteApartment(@PathVariable String id) { + return apartmentService.delete(id); + } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApiExceptionHandler.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApiExceptionHandler.java index ad15b99..09e24ab 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApiExceptionHandler.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/ApiExceptionHandler.java @@ -11,34 +11,34 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException; @RestControllerAdvice public class ApiExceptionHandler { - @ExceptionHandler(AttachmentTooLargeException.class) - public ResponseEntity handleAttachmentTooLarge(AttachmentTooLargeException exception) { - return error(HttpStatus.CONTENT_TOO_LARGE, exception.getMessage()); - } + @ExceptionHandler(AttachmentTooLargeException.class) + public ResponseEntity handleAttachmentTooLarge(AttachmentTooLargeException exception) { + return error(HttpStatus.CONTENT_TOO_LARGE, exception.getMessage()); + } - @ExceptionHandler(MaxUploadSizeExceededException.class) - public ResponseEntity handleMultipartTooLarge() { - return error(HttpStatus.CONTENT_TOO_LARGE, "Une pièce jointe ne peut pas dépasser 250 Mo."); - } + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity handleMultipartTooLarge() { + return error(HttpStatus.CONTENT_TOO_LARGE, "Une pièce jointe ne peut pas dépasser 250 Mo."); + } - @ExceptionHandler(InvalidPhoneNumberException.class) - public ResponseEntity handleInvalidPhone(InvalidPhoneNumberException exception) { - return error(HttpStatus.BAD_REQUEST, exception.getMessage()); - } + @ExceptionHandler(InvalidPhoneNumberException.class) + public ResponseEntity handleInvalidPhone(InvalidPhoneNumberException exception) { + return error(HttpStatus.BAD_REQUEST, exception.getMessage()); + } - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity 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 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 error(HttpStatus status, String message) { - return ResponseEntity.status(status).body(new ApiError(message)); - } + private ResponseEntity error(HttpStatus status, String message) { + return ResponseEntity.status(status).body(new ApiError(message)); + } - private record ApiError(String message) { - } + private record ApiError(String message) { + } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/LeaseController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/LeaseController.java index 82efdea..8f5851a 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/LeaseController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/LeaseController.java @@ -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 getAllLeases() { - return leaseRepository.findAll(); - } + @GetMapping + public List getAllLeases() { + return leaseService.getAll(); + } - @GetMapping("/{id}") - public ResponseEntity getLease(@PathVariable String id) { - return leaseRepository.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()); - } + @GetMapping("/{id}") + public ResponseEntity getLease(@PathVariable String id) { + return leaseService.getById(id); + } - @PostMapping - public ResponseEntity 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 createLease(@Valid @RequestBody LeaseRequest request) { + return leaseService.create(request); + } - @PutMapping("/{id}") - public ResponseEntity 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 updateLease(@PathVariable String id, + @Valid @RequestBody LeaseRequest request) { + return leaseService.update(id, request); + } - @DeleteMapping("/{id}") - public ResponseEntity deleteLease(@PathVariable String id) { - if (!leaseRepository.existsById(id)) { - return ResponseEntity.notFound().build(); - } - leaseRepository.deleteById(id); - return ResponseEntity.noContent().build(); - } + @DeleteMapping("/{id}") + public ResponseEntity deleteLease(@PathVariable String id) { + return leaseService.delete(id); + } - @GetMapping("/{id}/pdf") - public ResponseEntity 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 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 tenantsFor(List ids) { - List 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 downloadPdf(@PathVariable String id) { + return leaseService.downloadPdf(id); + } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/TenantController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/TenantController.java index 7fa448c..0f73294 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/TenantController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/TenantController.java @@ -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 getAllTenants() { - return tenantRepository.findAll(); - } + @GetMapping + public List getAllTenants() { + return tenantService.getAll(); + } - @GetMapping("/{id}") - public ResponseEntity getTenant(@PathVariable String id) { - return tenantRepository.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()); - } + @GetMapping("/{id}") + public ResponseEntity getTenant(@PathVariable String id) { + return tenantService.getById(id); + } - @PostMapping - public ResponseEntity 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 createTenant(@Valid @RequestBody TenantRequest request) { + return tenantService.create(request); + } - @PutMapping("/{id}") - public ResponseEntity 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 updateTenant(@PathVariable String id, + @Valid @RequestBody TenantRequest request) { + return tenantService.update(id, request); + } - @DeleteMapping("/{id}") - public ResponseEntity deleteTenant(@PathVariable String id) { - if (!tenantRepository.existsById(id)) { - return ResponseEntity.notFound().build(); - } - tenantRepository.deleteById(id); - return ResponseEntity.noContent().build(); - } + @DeleteMapping("/{id}") + public ResponseEntity deleteTenant(@PathVariable String id) { + return tenantService.delete(id); + } - @PostMapping(path = "/{tenantId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity 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 uploadAttachment(@PathVariable String tenantId, + @RequestPart("file") MultipartFile file) throws IOException { + return tenantService.uploadAttachment(tenantId, file); + } - @GetMapping("/{tenantId}/attachments/{attachmentId}") - public ResponseEntity 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 downloadAttachment(@PathVariable String tenantId, @PathVariable String attachmentId) + throws IOException { + return tenantService.downloadAttachment(tenantId, attachmentId); + } - @DeleteMapping("/{tenantId}/attachments/{attachmentId}") - public ResponseEntity 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 deleteAttachment(@PathVariable String tenantId, @PathVariable String attachmentId) + throws IOException { + return tenantService.deleteAttachment(tenantId, attachmentId); + } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java index f4e7804..ac1364f 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java @@ -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 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); + } } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentRequest.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentRequest.java new file mode 100644 index 0000000..9dea269 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentRequest.java @@ -0,0 +1,4 @@ +package fr.nessar.lmnporganizer.backend.dto; + +public record ApartmentRequest(String name, String address, double purchasePrice, String description) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentResponse.java new file mode 100644 index 0000000..2d0dbd8 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/ApartmentResponse.java @@ -0,0 +1,4 @@ +package fr.nessar.lmnporganizer.backend.dto; + +public record ApartmentResponse(String id, String name, String address, double purchasePrice, String description) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/GuarantorDto.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/GuarantorDto.java new file mode 100644 index 0000000..13015f6 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/GuarantorDto.java @@ -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 +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseRequest.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseRequest.java new file mode 100644 index 0000000..f22c033 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseRequest.java @@ -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 tenantIds, + List<@Valid GuarantorDto> guarantors, + LocalDate startDate, + LocalDate endDate, + BigDecimal monthlyRent, + BigDecimal monthlyCharges, + BigDecimal securityDeposit, + LeaseStatus status +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseResponse.java new file mode 100644 index 0000000..af9e12c --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/LeaseResponse.java @@ -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 tenantIds, + List guarantors, + LocalDate startDate, + LocalDate endDate, + BigDecimal monthlyRent, + BigDecimal monthlyCharges, + BigDecimal securityDeposit, + LeaseStatus status +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantAttachmentResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantAttachmentResponse.java new file mode 100644 index 0000000..72a13cf --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantAttachmentResponse.java @@ -0,0 +1,4 @@ +package fr.nessar.lmnporganizer.backend.dto; + +public record TenantAttachmentResponse(String id, String fileName, String contentType, long size) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantRequest.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantRequest.java new file mode 100644 index 0000000..25d688a --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantRequest.java @@ -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 +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantResponse.java new file mode 100644 index 0000000..85998e5 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/TenantResponse.java @@ -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 attachments +) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/UserResponse.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/UserResponse.java new file mode 100644 index 0000000..31f9655 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/dto/UserResponse.java @@ -0,0 +1,4 @@ +package fr.nessar.lmnporganizer.backend.dto; + +public record UserResponse(boolean authenticated, String username, String email) { +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/ApartmentMapper.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/ApartmentMapper.java new file mode 100644 index 0000000..2699f73 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/ApartmentMapper.java @@ -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 toResponses(List 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); +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/LeaseMapper.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/LeaseMapper.java new file mode 100644 index 0000000..94c2c68 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/LeaseMapper.java @@ -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 toResponses(List 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); +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/TenantMapper.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/TenantMapper.java new file mode 100644 index 0000000..924aeef --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/mapper/TenantMapper.java @@ -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 toResponses(List 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); +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java index db9d1a4..f0ebfeb 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/LeaseRepository.java @@ -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 { } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/TenantRepository.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/TenantRepository.java index 3995cc3..bb81d9a 100644 --- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/TenantRepository.java +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/repository/TenantRepository.java @@ -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 { } diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/ApartmentService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/ApartmentService.java new file mode 100644 index 0000000..bbe6f6e --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/ApartmentService.java @@ -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 getAll() { + return apartmentMapper.toResponses(apartmentRepository.findAll()); + } + + public ResponseEntity getById(String id) { + return apartmentRepository.findById(id).map(apartmentMapper::toResponse).map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + public ResponseEntity 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 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 delete(String id) { + if (!apartmentRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + apartmentRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/GreetingService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/GreetingService.java new file mode 100644 index 0000000..8afa87e --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/GreetingService.java @@ -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) + "!"; + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/LeaseService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/LeaseService.java new file mode 100644 index 0000000..62dcb5e --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/LeaseService.java @@ -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 getAll() { + return leaseMapper.toResponses(leaseRepository.findAll()); + } + + public ResponseEntity getById(String id) { + return leaseRepository.findById(id).map(leaseMapper::toResponse).map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + public ResponseEntity 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 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 delete(String id) { + if (!leaseRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + leaseRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + public ResponseEntity 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 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 tenantsFor(List ids) { + List 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()))); + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/TenantService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/TenantService.java new file mode 100644 index 0000000..e58c0bd --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/TenantService.java @@ -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 getAll() { + return tenantMapper.toResponses(tenantRepository.findAll()); + } + + public ResponseEntity getById(String id) { + return tenantRepository.findById(id).map(tenantMapper::toResponse).map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + public ResponseEntity 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 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 delete(String id) { + if (!tenantRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + tenantRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + public ResponseEntity 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 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 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; + } + } +} diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/UserService.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/UserService.java new file mode 100644 index 0000000..7dbf223 --- /dev/null +++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/service/UserService.java @@ -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); + } +} diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java index ac969f2..4fb00d1 100644 --- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java @@ -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 diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/LeaseControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/LeaseControllerTest.java index f607733..cfb1227 100644 --- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/LeaseControllerTest.java +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/LeaseControllerTest.java @@ -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")) diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/TenantControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/TenantControllerTest.java index bf86fc7..ddc4603 100644 --- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/TenantControllerTest.java +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/TenantControllerTest.java @@ -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 diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/mapper/UpdateMapperTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/mapper/UpdateMapperTest.java new file mode 100644 index 0000000..6ac5bc5 --- /dev/null +++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/mapper/UpdateMapperTest.java @@ -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()); + } +} diff --git a/frontend/src/app/services/apartment.service.ts b/frontend/src/app/services/apartment.service.ts index 7b50817..bbceb68 100644 --- a/frontend/src/app/services/apartment.service.ts +++ b/frontend/src/app/services/apartment.service.ts @@ -4,7 +4,6 @@ import { Observable } from 'rxjs'; export interface Apartment { id?: string; - bailleurId?: string; name: string; address: string; purchasePrice: number; diff --git a/frontend/src/app/services/lease.service.ts b/frontend/src/app/services/lease.service.ts index 27894f2..bb9ac54 100644 --- a/frontend/src/app/services/lease.service.ts +++ b/frontend/src/app/services/lease.service.ts @@ -13,7 +13,6 @@ export interface Guarantor { export interface Lease { id?: string; - landlordId?: string; apartmentId: string; tenantIds: string[]; guarantors: Guarantor[]; diff --git a/frontend/src/app/services/session.service.ts b/frontend/src/app/services/session.service.ts index 27e0426..d26be33 100644 --- a/frontend/src/app/services/session.service.ts +++ b/frontend/src/app/services/session.service.ts @@ -1,6 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject, signal } from '@angular/core'; +interface UserResponse { + authenticated: boolean; + username?: string; + email?: string; +} + @Injectable({ providedIn: 'root' }) export class SessionService { private readonly http = inject(HttpClient); @@ -9,7 +15,7 @@ export class SessionService { readonly email = signal(''); refresh(): void { - this.http.get<{ authenticated: boolean; username?: string; email?: string }>('/api/user').subscribe({ + this.http.get('/api/user').subscribe({ next: (user) => { this.isLoggedIn.set(user.authenticated); this.username.set(user.authenticated ? (user.username || 'User') : ''); diff --git a/frontend/src/app/services/tenant.service.ts b/frontend/src/app/services/tenant.service.ts index 9e9738d..1a00c1f 100644 --- a/frontend/src/app/services/tenant.service.ts +++ b/frontend/src/app/services/tenant.service.ts @@ -11,7 +11,6 @@ export interface TenantAttachment { export interface Tenant { id?: string; - landlordId?: string; firstName: string; lastName: string; email: string;