Merge remote-tracking branch 'origin/main' into azure-deploy
This commit is contained in:
@@ -2,6 +2,7 @@ package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.adoption.AdoptionRequest;
|
||||
import com.petshop.backend.dto.adoption.AdoptionResponse;
|
||||
import com.petshop.backend.dto.adoption.CustomerAdoptionRequest;
|
||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
@@ -81,6 +82,33 @@ public class AdoptionController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request));
|
||||
}
|
||||
|
||||
@PostMapping("/request")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(
|
||||
adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate())
|
||||
);
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> cancelAdoption(@PathVariable Long id) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String role = authentication.getAuthorities().stream()
|
||||
.findFirst()
|
||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||
.orElse(null);
|
||||
|
||||
Long customerId = null;
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
customerId = user.getId();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> updateAdoption(
|
||||
|
||||
@@ -98,6 +98,24 @@ public class AppointmentController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AppointmentResponse> cancelAppointment(@PathVariable Long id) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String role = authentication.getAuthorities().stream()
|
||||
.findFirst()
|
||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||
.orElse(null);
|
||||
|
||||
Long customerId = null;
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
customerId = user.getId();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AppointmentResponse> updateAppointment(
|
||||
|
||||
@@ -73,7 +73,8 @@ public class AuthController {
|
||||
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
|
||||
String username = trimToNull(request.getUsername());
|
||||
String email = trimToNull(request.getEmail());
|
||||
NameParts nameParts = splitFullName(request.getFullName());
|
||||
String firstName = trimToNull(request.getFirstName());
|
||||
String lastName = trimToNull(request.getLastName());
|
||||
String phone = normalizePhone(request.getPhone());
|
||||
|
||||
if (userRepository.findByUsername(username).isPresent()) {
|
||||
@@ -101,9 +102,9 @@ public class AuthController {
|
||||
user.setUsername(username);
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setEmail(email);
|
||||
user.setFirstName(nameParts.firstName());
|
||||
user.setLastName(nameParts.lastName());
|
||||
user.setFullName(nameParts.fullName());
|
||||
user.setFirstName(firstName);
|
||||
user.setLastName(lastName);
|
||||
user.setFullName(joinFullName(firstName, lastName));
|
||||
user.setPhone(phone);
|
||||
user.setRole(User.Role.CUSTOMER);
|
||||
user.setActive(true);
|
||||
@@ -208,11 +209,16 @@ public class AuthController {
|
||||
user.setEmail(email);
|
||||
}
|
||||
|
||||
if (request.getFullName() != null) {
|
||||
NameParts nameParts = splitFullName(request.getFullName());
|
||||
user.setFirstName(nameParts.firstName());
|
||||
user.setLastName(nameParts.lastName());
|
||||
user.setFullName(nameParts.fullName());
|
||||
String firstName = trimToNull(request.getFirstName());
|
||||
if (firstName != null) {
|
||||
user.setFirstName(firstName);
|
||||
}
|
||||
String lastName = trimToNull(request.getLastName());
|
||||
if (lastName != null) {
|
||||
user.setLastName(lastName);
|
||||
}
|
||||
if (firstName != null || lastName != null) {
|
||||
user.setFullName(joinFullName(user.getFirstName(), user.getLastName()));
|
||||
}
|
||||
|
||||
if (request.getPhone() != null) {
|
||||
@@ -252,6 +258,8 @@ public class AuthController {
|
||||
return new UserInfoResponse(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getFirstName(),
|
||||
user.getLastName(),
|
||||
user.getEmail(),
|
||||
fullName,
|
||||
user.getPhone(),
|
||||
|
||||
@@ -38,8 +38,8 @@ public class MyPetController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<MyPetResponse>> getMyPets() {
|
||||
return ResponseEntity.ok(petService.getMyPets(currentUserId()));
|
||||
public ResponseEntity<List<MyPetResponse>> getMyPets(@RequestParam(required = false) String status) {
|
||||
return ResponseEntity.ok(petService.getMyPets(currentUserId(), status));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
||||
@@ -23,7 +23,6 @@ public class StoreController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<Page<StoreResponse>> getAllStores(
|
||||
@RequestParam(required = false) String q,
|
||||
Pageable pageable) {
|
||||
@@ -31,7 +30,6 @@ public class StoreController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<StoreResponse> getStoreById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(storeService.getStoreById(id));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.petshop.backend.dto.adoption;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class CustomerAdoptionRequest {
|
||||
|
||||
@NotNull(message = "Pet ID is required")
|
||||
private Long petId;
|
||||
|
||||
private Long employeeId;
|
||||
|
||||
private Long sourceStoreId;
|
||||
|
||||
@NotNull(message = "Appointment date is required")
|
||||
private LocalDate adoptionDate;
|
||||
|
||||
public Long getPetId() {
|
||||
return petId;
|
||||
}
|
||||
|
||||
public void setPetId(Long petId) {
|
||||
this.petId = petId;
|
||||
}
|
||||
|
||||
public Long getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public void setEmployeeId(Long employeeId) {
|
||||
this.employeeId = employeeId;
|
||||
}
|
||||
|
||||
public Long getSourceStoreId() {
|
||||
return sourceStoreId;
|
||||
}
|
||||
|
||||
public void setSourceStoreId(Long sourceStoreId) {
|
||||
this.sourceStoreId = sourceStoreId;
|
||||
}
|
||||
|
||||
public LocalDate getAdoptionDate() {
|
||||
return adoptionDate;
|
||||
}
|
||||
|
||||
public void setAdoptionDate(LocalDate adoptionDate) {
|
||||
this.adoptionDate = adoptionDate;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,11 @@ public class ProfileUpdateRequest {
|
||||
@Email(message = "Email must be valid")
|
||||
private String email;
|
||||
|
||||
@Size(max = 100, message = "Full name must not exceed 100 characters")
|
||||
private String fullName;
|
||||
@Size(max = 50, message = "First name must not exceed 50 characters")
|
||||
private String firstName;
|
||||
|
||||
@Size(max = 50, message = "Last name must not exceed 50 characters")
|
||||
private String lastName;
|
||||
|
||||
@Size(max = 20, message = "Phone must not exceed 20 characters")
|
||||
private String phone;
|
||||
@@ -36,12 +39,20 @@ public class ProfileUpdateRequest {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
@@ -67,14 +78,15 @@ public class ProfileUpdateRequest {
|
||||
ProfileUpdateRequest that = (ProfileUpdateRequest) o;
|
||||
return Objects.equals(username, that.username) &&
|
||||
Objects.equals(email, that.email) &&
|
||||
Objects.equals(fullName, that.fullName) &&
|
||||
Objects.equals(firstName, that.firstName) &&
|
||||
Objects.equals(lastName, that.lastName) &&
|
||||
Objects.equals(phone, that.phone) &&
|
||||
Objects.equals(password, that.password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(username, email, fullName, phone, password);
|
||||
return Objects.hash(username, email, firstName, lastName, phone, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +94,8 @@ public class ProfileUpdateRequest {
|
||||
return "ProfileUpdateRequest{" +
|
||||
"username='" + username + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", fullName='" + fullName + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", phone='" + phone + '\'' +
|
||||
", password='" + password + '\'' +
|
||||
'}';
|
||||
|
||||
@@ -18,9 +18,13 @@ public class RegisterRequest {
|
||||
@Email(message = "Email must be valid")
|
||||
private String email;
|
||||
|
||||
@NotBlank(message = "Full name is required")
|
||||
@Size(max = 100, message = "Full name must not exceed 100 characters")
|
||||
private String fullName;
|
||||
@NotBlank(message = "First name is required")
|
||||
@Size(max = 50, message = "First name must not exceed 50 characters")
|
||||
private String firstName;
|
||||
|
||||
@NotBlank(message = "Last name is required")
|
||||
@Size(max = 50, message = "Last name must not exceed 50 characters")
|
||||
private String lastName;
|
||||
|
||||
@NotBlank(message = "Phone is required")
|
||||
@Size(max = 20, message = "Phone must not exceed 20 characters")
|
||||
@@ -50,12 +54,20 @@ public class RegisterRequest {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
@@ -74,13 +86,14 @@ public class RegisterRequest {
|
||||
return Objects.equals(username, that.username) &&
|
||||
Objects.equals(password, that.password) &&
|
||||
Objects.equals(email, that.email) &&
|
||||
Objects.equals(fullName, that.fullName) &&
|
||||
Objects.equals(firstName, that.firstName) &&
|
||||
Objects.equals(lastName, that.lastName) &&
|
||||
Objects.equals(phone, that.phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(username, password, email, fullName, phone);
|
||||
return Objects.hash(username, password, email, firstName, lastName, phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +102,8 @@ public class RegisterRequest {
|
||||
"username='" + username + '\'' +
|
||||
", password='" + password + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", fullName='" + fullName + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", phone='" + phone + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.util.Objects;
|
||||
public class UserInfoResponse {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String email;
|
||||
private String fullName;
|
||||
private String phone;
|
||||
@@ -17,9 +19,11 @@ public class UserInfoResponse {
|
||||
public UserInfoResponse() {
|
||||
}
|
||||
|
||||
public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
|
||||
public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.fullName = fullName;
|
||||
this.phone = phone;
|
||||
@@ -46,6 +50,22 @@ public class UserInfoResponse {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.petshop.backend.dto.pet;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
@@ -16,6 +18,10 @@ public class MyPetRequest {
|
||||
@Size(max = 50, message = "Breed must not exceed 50 characters")
|
||||
private String breed;
|
||||
|
||||
@Min(value = 0, message = "Age must be 0 or greater")
|
||||
@Max(value = 100, message = "Age must not exceed 100")
|
||||
private Integer petAge;
|
||||
|
||||
public String getPetName() {
|
||||
return petName;
|
||||
}
|
||||
@@ -39,4 +45,12 @@ public class MyPetRequest {
|
||||
public void setBreed(String breed) {
|
||||
this.breed = breed;
|
||||
}
|
||||
|
||||
public Integer getPetAge() {
|
||||
return petAge;
|
||||
}
|
||||
|
||||
public void setPetAge(Integer petAge) {
|
||||
this.petAge = petAge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ public class MyPetResponse {
|
||||
private String petName;
|
||||
private String species;
|
||||
private String breed;
|
||||
private Integer petAge;
|
||||
private String imageUrl;
|
||||
private String petStatus;
|
||||
|
||||
public MyPetResponse() {
|
||||
}
|
||||
|
||||
public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) {
|
||||
public MyPetResponse(Long customerPetId, String petName, String species, String breed, Integer petAge, String imageUrl, String petStatus) {
|
||||
this.customerPetId = customerPetId;
|
||||
this.petName = petName;
|
||||
this.species = species;
|
||||
this.breed = breed;
|
||||
this.petAge = petAge;
|
||||
this.imageUrl = imageUrl;
|
||||
this.petStatus = petStatus;
|
||||
}
|
||||
|
||||
public Long getCustomerPetId() {
|
||||
@@ -51,6 +55,14 @@ public class MyPetResponse {
|
||||
this.breed = breed;
|
||||
}
|
||||
|
||||
public Integer getPetAge() {
|
||||
return petAge;
|
||||
}
|
||||
|
||||
public void setPetAge(Integer petAge) {
|
||||
this.petAge = petAge;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
@@ -58,4 +70,12 @@ public class MyPetResponse {
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getPetStatus() {
|
||||
return petStatus;
|
||||
}
|
||||
|
||||
public void setPetStatus(String petStatus) {
|
||||
this.petStatus = petStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,5 +51,7 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
|
||||
@Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'")
|
||||
List<Appointment> findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime);
|
||||
|
||||
List<Appointment> findByPet_Id(Long petId);
|
||||
|
||||
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/stores/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-species").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-breeds").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/stores").permitAll()
|
||||
@@ -101,7 +102,7 @@ public class SecurityConfig {
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
|
||||
@@ -158,6 +158,57 @@ public class AdoptionService {
|
||||
return mapToResponse(adoption);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId, LocalDate adoptionDate) {
|
||||
Pet pet = petRepository.findById(petId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Pet not found"));
|
||||
|
||||
// Verify the pet is actually located at the claimed store
|
||||
if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) {
|
||||
throw new IllegalArgumentException("The specified pet is not located at the selected store.");
|
||||
}
|
||||
|
||||
// Verify the pet is available for adoption
|
||||
validatePetAvailability(pet, null, null);
|
||||
|
||||
User customer = userRepository.findById(customerId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
|
||||
User employee = resolveAdoptionEmployee(employeeId);
|
||||
StoreLocation sourceStore = storeRepository.findById(sourceStoreId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found"));
|
||||
|
||||
Adoption adoption = new Adoption();
|
||||
adoption.setPet(pet);
|
||||
adoption.setCustomer(customer);
|
||||
adoption.setEmployee(employee);
|
||||
adoption.setSourceStore(sourceStore);
|
||||
adoption.setAdoptionDate(adoptionDate);
|
||||
adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING);
|
||||
|
||||
adoption = adoptionRepository.save(adoption);
|
||||
syncPetStatus(pet, ADOPTION_STATUS_PENDING, adoption.getAdoptionId(), customer);
|
||||
return mapToResponse(adoption);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdoptionResponse cancelAdoption(Long adoptionId, Long requestingCustomerId) {
|
||||
Adoption adoption = adoptionRepository.findById(adoptionId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + adoptionId));
|
||||
|
||||
if (requestingCustomerId != null && !adoption.getCustomer().getId().equals(requestingCustomerId)) {
|
||||
throw new ResourceNotFoundException("Adoption not found");
|
||||
}
|
||||
|
||||
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) {
|
||||
throw new IllegalArgumentException("Only pending adoptions can be cancelled");
|
||||
}
|
||||
|
||||
adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED);
|
||||
adoption = adoptionRepository.save(adoption);
|
||||
syncPetStatus(adoption.getPet(), ADOPTION_STATUS_CANCELLED, adoption.getAdoptionId(), adoption.getCustomer());
|
||||
return mapToResponse(adoption);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAdoption(Long id) {
|
||||
Adoption adoption = adoptionRepository.findById(id)
|
||||
|
||||
@@ -109,6 +109,22 @@ public class AppointmentService {
|
||||
Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null;
|
||||
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||
|
||||
// Customers must supply a pet that is Adopted and owned by them
|
||||
if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) {
|
||||
if (pet == null) {
|
||||
throw new IllegalArgumentException("A pet must be selected for your appointment");
|
||||
}
|
||||
if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) {
|
||||
throw new IllegalArgumentException("The selected pet does not belong to your account");
|
||||
}
|
||||
String petStatus = pet.getPetStatus();
|
||||
if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) {
|
||||
throw new IllegalArgumentException("Only your own pets can be booked for appointments");
|
||||
}
|
||||
}
|
||||
|
||||
validateSpeciesServiceCompatibility(pet, service);
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validatePetServiceCompatibility(pet, service);
|
||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||
@@ -167,6 +183,23 @@ public class AppointmentService {
|
||||
return mapToResponse(appointment);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AppointmentResponse cancelAppointment(Long appointmentId, Long requestingCustomerId) {
|
||||
Appointment appointment = appointmentRepository.findById(appointmentId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + appointmentId));
|
||||
|
||||
if (requestingCustomerId != null && !appointment.getCustomer().getId().equals(requestingCustomerId)) {
|
||||
throw new ResourceNotFoundException("Appointment not found");
|
||||
}
|
||||
|
||||
if (!"Booked".equalsIgnoreCase(appointment.getAppointmentStatus())) {
|
||||
throw new IllegalArgumentException("Only booked appointments can be cancelled");
|
||||
}
|
||||
|
||||
appointment.setAppointmentStatus("Cancelled");
|
||||
return mapToResponse(appointmentRepository.save(appointment));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAppointment(Long id) {
|
||||
if (!appointmentRepository.existsById(id)) {
|
||||
@@ -350,6 +383,32 @@ public class AppointmentService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
|
||||
if (pet == null || service == null) return;
|
||||
String species = pet.getPetSpecies();
|
||||
if (species == null) return;
|
||||
String serviceName = service.getServiceName().toLowerCase();
|
||||
|
||||
switch (species.toLowerCase()) {
|
||||
case "bird":
|
||||
if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Service '" + service.getServiceName() + "' is not available for birds. " +
|
||||
"Allowed services: Wing Clipping, Beak and Nail Care.");
|
||||
}
|
||||
break;
|
||||
case "fish":
|
||||
if (!serviceName.contains("aquarium health")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Service '" + service.getServiceName() + "' is not available for fish. " +
|
||||
"Allowed service: Aquarium Health Check.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateStoreAccess(Long requestedStoreId, User user) {
|
||||
if (user.getRole() != User.Role.STAFF) {
|
||||
return;
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||
import com.petshop.backend.security.AppPrincipal;
|
||||
import com.petshop.backend.repository.AdoptionRepository;
|
||||
import com.petshop.backend.repository.AppointmentRepository;
|
||||
import com.petshop.backend.repository.PetRepository;
|
||||
import com.petshop.backend.repository.StoreRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
@@ -35,13 +36,15 @@ public class PetService {
|
||||
|
||||
private final PetRepository petRepository;
|
||||
private final AdoptionRepository adoptionRepository;
|
||||
private final AppointmentRepository appointmentRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final CatalogImageStorageService catalogImageStorageService;
|
||||
|
||||
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
|
||||
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
|
||||
this.petRepository = petRepository;
|
||||
this.adoptionRepository = adoptionRepository;
|
||||
this.appointmentRepository = appointmentRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
@@ -87,8 +90,9 @@ public class PetService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<MyPetResponse> getMyPets(Long ownerUserId) {
|
||||
public List<MyPetResponse> getMyPets(Long ownerUserId, String status) {
|
||||
return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream()
|
||||
.filter(p -> status == null || status.isBlank() || status.equalsIgnoreCase(p.getPetStatus()))
|
||||
.map(this::mapToMyPetResponse)
|
||||
.toList();
|
||||
}
|
||||
@@ -117,6 +121,18 @@ public class PetService {
|
||||
@Transactional
|
||||
public void deleteMyPet(Long ownerUserId, Long petId) {
|
||||
Pet pet = findOwnedPet(ownerUserId, petId);
|
||||
List<com.petshop.backend.entity.Appointment> linkedAppointments = appointmentRepository.findByPet_Id(petId);
|
||||
boolean hasBooked = linkedAppointments.stream()
|
||||
.anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus()));
|
||||
if (hasBooked) {
|
||||
throw new IllegalArgumentException(
|
||||
"Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database.");
|
||||
}
|
||||
// Nullify the pet reference on non-booked appointments to avoid FK constraint violations
|
||||
for (com.petshop.backend.entity.Appointment appt : linkedAppointments) {
|
||||
appt.setPet(null);
|
||||
}
|
||||
appointmentRepository.saveAll(linkedAppointments);
|
||||
deleteStoredImageIfPresent(pet.getImageUrl());
|
||||
petRepository.delete(pet);
|
||||
}
|
||||
@@ -341,7 +357,9 @@ public class PetService {
|
||||
pet.getPetName(),
|
||||
pet.getPetSpecies(),
|
||||
pet.getPetBreed(),
|
||||
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null
|
||||
pet.getPetAge(),
|
||||
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
|
||||
pet.getPetStatus()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,7 +367,7 @@ public class PetService {
|
||||
pet.setPetName(request.getPetName().trim());
|
||||
pet.setPetSpecies(request.getSpecies().trim());
|
||||
pet.setPetBreed(normalizeOptional(request.getBreed()));
|
||||
pet.setPetAge(null);
|
||||
pet.setPetAge(request.getPetAge());
|
||||
pet.setPetPrice(null);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user