Appointments, account stuff, adopt a pet changes

This commit is contained in:
augmentedpotato
2026-03-30 05:38:15 -06:00
parent cce97b7509
commit 3b8b1e9b97
30 changed files with 2611 additions and 48 deletions

View File

@@ -9,6 +9,7 @@ import com.petshop.backend.dto.auth.RegisterResponse;
import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
import com.petshop.backend.repository.UserRepository;
@@ -47,8 +48,9 @@ public class AuthController {
private final EmployeeRepository employeeRepository;
private final EmployeeStoreRepository employeeStoreRepository;
private final AvatarStorageService avatarStorageService;
private final CustomerRepository customerRepository;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService, CustomerRepository customerRepository) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
@@ -57,6 +59,7 @@ public class AuthController {
this.employeeRepository = employeeRepository;
this.employeeStoreRepository = employeeStoreRepository;
this.avatarStorageService = avatarStorageService;
this.customerRepository = customerRepository;
}
@PostMapping("/register")
@@ -147,6 +150,7 @@ public class AuthController {
User user = getAuthenticatedUser();
EmployeeStore employeeStore = resolveEmployeeStore(user);
Long customerId = resolveCustomerId(user);
return ResponseEntity.ok(new UserInfoResponse(
user.getId(),
@@ -156,6 +160,7 @@ public class AuthController {
user.getPhone(),
avatarStorageService.toOwnerAvatarUrl(user),
user.getRole().name(),
customerId,
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
employeeStore != null ? employeeStore.getStore().getStoreName() : null
));
@@ -216,6 +221,7 @@ public class AuthController {
userBusinessLinkageService.syncLinkedRecords(updatedUser);
EmployeeStore employeeStore = resolveEmployeeStore(updatedUser);
Long customerId = resolveCustomerId(updatedUser);
return ResponseEntity.ok(new UserInfoResponse(
updatedUser.getId(),
@@ -225,6 +231,7 @@ public class AuthController {
updatedUser.getPhone(),
avatarStorageService.toOwnerAvatarUrl(updatedUser),
updatedUser.getRole().name(),
customerId,
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
employeeStore != null ? employeeStore.getStore().getStoreName() : null
));
@@ -240,6 +247,12 @@ public class AuthController {
.orElse(null);
}
private Long resolveCustomerId(User user) {
return customerRepository.findByUserId(user.getId())
.map(c -> c.getCustomerId())
.orElse(null);
}
private String trimToNull(String value) {
if (value == null) {
return null;

View File

@@ -0,0 +1,118 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.customerpet.CustomerPetRequest;
import com.petshop.backend.dto.customerpet.CustomerPetResponse;
import com.petshop.backend.service.CatalogImageStorageService;
import com.petshop.backend.service.CustomerPetService;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.util.AuthenticationHelper;
import jakarta.validation.Valid;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/my-pets")
@PreAuthorize("hasRole('CUSTOMER')")
public class CustomerPetController {
private final CustomerPetService customerPetService;
private final CustomerPetRepository customerPetRepository;
private final CustomerRepository customerRepository;
private final UserRepository userRepository;
private final CatalogImageStorageService catalogImageStorageService;
public CustomerPetController(CustomerPetService customerPetService,
CustomerPetRepository customerPetRepository,
CustomerRepository customerRepository,
UserRepository userRepository,
CatalogImageStorageService catalogImageStorageService) {
this.customerPetService = customerPetService;
this.customerPetRepository = customerPetRepository;
this.customerRepository = customerRepository;
this.userRepository = userRepository;
this.catalogImageStorageService = catalogImageStorageService;
}
@GetMapping
public ResponseEntity<List<CustomerPetResponse>> getMyPets() {
return ResponseEntity.ok(customerPetService.getMyPets());
}
@PostMapping
public ResponseEntity<CustomerPetResponse> createPet(@Valid @RequestBody CustomerPetRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(customerPetService.createPet(request));
}
@PutMapping("/{id}")
public ResponseEntity<CustomerPetResponse> updatePet(@PathVariable Long id, @Valid @RequestBody CustomerPetRequest request) {
return ResponseEntity.ok(customerPetService.updatePet(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePet(@PathVariable Long id) {
customerPetService.deletePet(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id}/image")
public ResponseEntity<?> uploadImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) {
try {
return ResponseEntity.ok(customerPetService.uploadImage(id, image));
}
catch (IllegalArgumentException ex) {
Map<String, String> error = new HashMap<>();
error.put("message", ex.getMessage());
return ResponseEntity.badRequest().body(error);
}
catch (IOException ex) {
Map<String, String> error = new HashMap<>();
error.put("message", "Failed to upload image: " + ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
@GetMapping("/{id}/image")
public ResponseEntity<Resource> getImage(@PathVariable Long id) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElse(null);
if (pet == null || pet.getImageUrl() == null || pet.getImageUrl().isBlank()) {
return ResponseEntity.notFound().build();
}
Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl());
MediaType mediaType = catalogImageStorageService.resolveMediaType(resource);
return ResponseEntity.ok().contentType(mediaType).body(resource);
}
@DeleteMapping("/{id}/image")
public ResponseEntity<CustomerPetResponse> deleteImage(@PathVariable Long id) {
return ResponseEntity.ok(customerPetService.deleteImage(id));
}
}

View File

@@ -1,6 +1,5 @@
package com.petshop.backend.dto.appointment;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalTime;
@@ -26,9 +25,10 @@ public class AppointmentRequest {
@NotNull(message = "Appointment status is required")
private String appointmentStatus;
@NotEmpty(message = "At least one pet must be specified")
private List<Long> petIds;
private List<Long> customerPetIds;
public Long getCustomerId() {
return customerId;
}
@@ -85,6 +85,14 @@ public class AppointmentRequest {
this.petIds = petIds;
}
public List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -96,12 +104,13 @@ public class AppointmentRequest {
Objects.equals(appointmentDate, that.appointmentDate) &&
Objects.equals(appointmentTime, that.appointmentTime) &&
Objects.equals(appointmentStatus, that.appointmentStatus) &&
Objects.equals(petIds, that.petIds);
Objects.equals(petIds, that.petIds) &&
Objects.equals(customerPetIds, that.customerPetIds);
}
@Override
public int hashCode() {
return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds);
return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds);
}
@Override
@@ -114,6 +123,7 @@ public class AppointmentRequest {
", appointmentTime=" + appointmentTime +
", appointmentStatus='" + appointmentStatus + '\'' +
", petIds=" + petIds +
", customerPetIds=" + customerPetIds +
'}';
}
}

View File

@@ -19,6 +19,8 @@ public class AppointmentResponse {
private String appointmentStatus;
private List<String> petNames;
private List<Long> petIds;
private List<String> customerPetNames;
private List<Long> customerPetIds;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@@ -138,6 +140,24 @@ public class AppointmentResponse {
this.petIds = petIds;
}
public List<String> getCustomerPetNames() {
return customerPetNames;
}
public void setCustomerPetNames(List<String> customerPetNames) {
this.customerPetNames = customerPetNames;
}
public List<Long> getCustomerPetIds() {
return customerPetIds;
}
public void setCustomerPetIds(List<Long> customerPetIds) {
this.customerPetIds = customerPetIds;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View File

@@ -10,13 +10,14 @@ public class UserInfoResponse {
private String phone;
private String avatarUrl;
private String role;
private Long customerId;
private Long storeId;
private String storeName;
public UserInfoResponse() {
}
public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long storeId, String storeName) {
public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
this.id = id;
this.username = username;
this.email = email;
@@ -24,6 +25,7 @@ public class UserInfoResponse {
this.phone = phone;
this.avatarUrl = avatarUrl;
this.role = role;
this.customerId = customerId;
this.storeId = storeId;
this.storeName = storeName;
}
@@ -84,6 +86,15 @@ public class UserInfoResponse {
this.role = role;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStoreId() {
return storeId;
}
@@ -112,13 +123,14 @@ public class UserInfoResponse {
Objects.equals(phone, that.phone) &&
Objects.equals(avatarUrl, that.avatarUrl) &&
Objects.equals(role, that.role) &&
Objects.equals(customerId, that.customerId) &&
Objects.equals(storeId, that.storeId) &&
Objects.equals(storeName, that.storeName);
}
@Override
public int hashCode() {
return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, storeId, storeName);
return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, customerId, storeId, storeName);
}
@Override
@@ -131,6 +143,7 @@ public class UserInfoResponse {
", phone='" + phone + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role='" + role + '\'' +
", customerId=" + customerId +
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
'}';

View File

@@ -0,0 +1,66 @@
package com.petshop.backend.dto.customerpet;
import jakarta.validation.constraints.NotBlank;
import java.util.Objects;
public class CustomerPetRequest {
@NotBlank(message = "Pet name is required")
private String petName;
@NotBlank(message = "Species is required")
private String species;
private String breed;
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomerPetRequest that = (CustomerPetRequest) o;
return Objects.equals(petName, that.petName) && Objects.equals(species, that.species) && Objects.equals(breed, that.breed);
}
@Override
public int hashCode() {
return Objects.hash(petName, species, breed);
}
}

View File

@@ -0,0 +1,123 @@
package com.petshop.backend.dto.customerpet;
import java.time.LocalDateTime;
import java.util.Objects;
public class CustomerPetResponse {
private Long customerPetId;
private Long customerId;
private String petName;
private String species;
private String breed;
private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public CustomerPetResponse() {
}
public CustomerPetResponse(Long customerPetId, Long customerId, String petName, String species, String breed, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.customerPetId = customerPetId;
this.customerId = customerId;
this.petName = petName;
this.species = species;
this.breed = breed;
this.imageUrl = imageUrl;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getCustomerPetId() {
return customerPetId;
}
public void setCustomerPetId(Long customerPetId) {
this.customerPetId = customerPetId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomerPetResponse that = (CustomerPetResponse) o;
return Objects.equals(customerPetId, that.customerPetId);
}
@Override
public int hashCode() {
return Objects.hash(customerPetId);
}
}

View File

@@ -48,6 +48,14 @@ public class Appointment {
)
private Set<Pet> pets = new HashSet<>();
@ManyToMany
@JoinTable(
name = "appointment_customer_pet",
joinColumns = @JoinColumn(name = "appointment_id"),
inverseJoinColumns = @JoinColumn(name = "customer_pet_id")
)
private Set<CustomerPet> customerPets = new HashSet<>();
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -136,6 +144,15 @@ public class Appointment {
this.pets = pets;
}
public Set<CustomerPet> getCustomerPets() {
return customerPets;
}
public void setCustomerPets(Set<CustomerPet> customerPets) {
this.customerPets = customerPets;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View File

@@ -0,0 +1,137 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "customer_pet")
public class CustomerPet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "customer_pet_id")
private Long customerPetId;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@Column(name = "pet_name", nullable = false, length = 50)
private String petName;
@Column(nullable = false, length = 50)
private String species;
@Column(length = 50)
private String breed;
@Column(name = "image_url", length = 255)
private String imageUrl;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public CustomerPet() {
}
public Long getCustomerPetId() {
return customerPetId;
}
public void setCustomerPetId(Long customerPetId) {
this.customerPetId = customerPetId;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public String getPetName() {
return petName;
}
public void setPetName(String petName) {
this.petName = petName;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomerPet that = (CustomerPet) o;
return Objects.equals(customerPetId, that.customerPetId);
}
@Override
public int hashCode() {
return Objects.hash(customerPetId);
}
}

View File

@@ -0,0 +1,16 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.CustomerPet;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CustomerPetRepository extends JpaRepository<CustomerPet, Long> {
List<CustomerPet> findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId);
Optional<CustomerPet> findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId);
}

View File

@@ -5,6 +5,7 @@ import com.petshop.backend.dto.appointment.AppointmentResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Appointment;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.Pet;
@@ -12,6 +13,7 @@ import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
@@ -40,6 +42,7 @@ public class AppointmentService {
private final AppointmentRepository appointmentRepository;
private final CustomerRepository customerRepository;
private final CustomerPetRepository customerPetRepository;
private final ServiceRepository serviceRepository;
private final PetRepository petRepository;
private final StoreRepository storeRepository;
@@ -47,9 +50,10 @@ public class AppointmentService {
private final EmployeeRepository employeeRepository;
private final EmployeeStoreRepository employeeStoreRepository;
public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) {
public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, CustomerPetRepository customerPetRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) {
this.appointmentRepository = appointmentRepository;
this.customerRepository = customerRepository;
this.customerPetRepository = customerPetRepository;
this.serviceRepository = serviceRepository;
this.petRepository = petRepository;
this.storeRepository = storeRepository;
@@ -107,7 +111,16 @@ public class AppointmentService {
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
Set<Pet> pets = fetchPets(request.getPetIds());
boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty();
boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty();
if (!hasPetIds && !hasCustomerPetIds) {
throw new IllegalArgumentException("Please specify at least one pet.");
}
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>();
Appointment appointment = new Appointment();
appointment.setCustomer(customer);
@@ -117,6 +130,7 @@ public class AppointmentService {
appointment.setAppointmentTime(request.getAppointmentTime());
appointment.setAppointmentStatus(request.getAppointmentStatus());
appointment.setPets(pets);
appointment.setCustomerPets(customerPets);
appointment = appointmentRepository.save(appointment);
return mapToResponse(appointment);
@@ -141,7 +155,16 @@ public class AppointmentService {
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
Set<Pet> pets = fetchPets(request.getPetIds());
boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty();
boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty();
if (!hasPetIds && !hasCustomerPetIds) {
throw new IllegalArgumentException("Please specify at least one pet.");
}
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>();
appointment.setCustomer(customer);
appointment.setStore(store);
@@ -150,6 +173,7 @@ public class AppointmentService {
appointment.setAppointmentTime(request.getAppointmentTime());
appointment.setAppointmentStatus(request.getAppointmentStatus());
appointment.setPets(pets);
appointment.setCustomerPets(customerPets);
appointment = appointmentRepository.save(appointment);
return mapToResponse(appointment);
@@ -213,6 +237,17 @@ public class AppointmentService {
return pets;
}
private Set<CustomerPet> fetchCustomerPets(List<Long> customerPetIds) {
Set<CustomerPet> customerPets = new HashSet<>();
for (Long customerPetId : customerPetIds) {
CustomerPet customerPet = customerPetRepository.findById(customerPetId)
.orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId));
customerPets.add(customerPet);
}
return customerPets;
}
private AppointmentResponse mapToResponse(Appointment appointment) {
List<String> petNames = appointment.getPets().stream()
.map(Pet::getPetName)
@@ -222,22 +257,33 @@ public class AppointmentService {
.map(Pet::getPetId)
.collect(Collectors.toList());
return new AppointmentResponse(
appointment.getAppointmentId(),
appointment.getCustomer().getCustomerId(),
appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName(),
appointment.getStore().getStoreId(),
appointment.getStore().getStoreName(),
appointment.getService().getServiceId(),
appointment.getService().getServiceName(),
appointment.getAppointmentDate(),
appointment.getAppointmentTime(),
appointment.getAppointmentStatus(),
petNames,
petIds,
appointment.getCreatedAt(),
appointment.getUpdatedAt()
);
List<String> customerPetNames = appointment.getCustomerPets().stream()
.map(CustomerPet::getPetName)
.collect(Collectors.toList());
List<Long> customerPetIds = appointment.getCustomerPets().stream()
.map(CustomerPet::getCustomerPetId)
.collect(Collectors.toList());
AppointmentResponse response = new AppointmentResponse();
response.setAppointmentId(appointment.getAppointmentId());
response.setCustomerId(appointment.getCustomer().getCustomerId());
response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName());
response.setStoreId(appointment.getStore().getStoreId());
response.setStoreName(appointment.getStore().getStoreName());
response.setServiceId(appointment.getService().getServiceId());
response.setServiceName(appointment.getService().getServiceName());
response.setAppointmentDate(appointment.getAppointmentDate());
response.setAppointmentTime(appointment.getAppointmentTime());
response.setAppointmentStatus(appointment.getAppointmentStatus());
response.setPetNames(petNames);
response.setPetIds(petIds);
response.setCustomerPetNames(customerPetNames);
response.setCustomerPetIds(customerPetIds);
response.setCreatedAt(appointment.getCreatedAt());
response.setUpdatedAt(appointment.getUpdatedAt());
return response;
}
private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {

View File

@@ -0,0 +1,163 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.customerpet.CustomerPetRequest;
import com.petshop.backend.dto.customerpet.CustomerPetResponse;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
public class CustomerPetService {
private final CustomerPetRepository customerPetRepository;
private final CustomerRepository customerRepository;
private final UserRepository userRepository;
private final CatalogImageStorageService catalogImageStorageService;
public CustomerPetService(CustomerPetRepository customerPetRepository,
CustomerRepository customerRepository,
UserRepository userRepository,
CatalogImageStorageService catalogImageStorageService) {
this.customerPetRepository = customerPetRepository;
this.customerRepository = customerRepository;
this.userRepository = userRepository;
this.catalogImageStorageService = catalogImageStorageService;
}
@Transactional(readOnly = true)
public List<CustomerPetResponse> getMyPets() {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
return customerPetRepository.findByCustomerCustomerIdOrderByCreatedAtDesc(customer.getCustomerId())
.stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
@Transactional
public CustomerPetResponse createPet(CustomerPetRequest request) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = new CustomerPet();
pet.setCustomer(customer);
pet.setPetName(request.getPetName());
pet.setSpecies(request.getSpecies());
pet.setBreed(request.getBreed());
pet = customerPetRepository.save(pet);
return mapToResponse(pet);
}
@Transactional
public CustomerPetResponse updatePet(Long id, CustomerPetRequest request) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
pet.setPetName(request.getPetName());
pet.setSpecies(request.getSpecies());
pet.setBreed(request.getBreed());
pet = customerPetRepository.save(pet);
return mapToResponse(pet);
}
@Transactional
public void deletePet(Long id) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
deleteStoredImageIfPresent(pet.getImageUrl());
customerPetRepository.delete(pet);
}
@Transactional
public CustomerPetResponse uploadImage(Long id, MultipartFile file) throws IOException {
validateImageFile(file);
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
deleteStoredImageIfPresent(pet.getImageUrl());
pet.setImageUrl(catalogImageStorageService.storePetImage(file));
return mapToResponse(customerPetRepository.save(pet));
}
@Transactional
public CustomerPetResponse deleteImage(Long id) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
deleteStoredImageIfPresent(pet.getImageUrl());
pet.setImageUrl(null);
return mapToResponse(customerPetRepository.save(pet));
}
private CustomerPetResponse mapToResponse(CustomerPet pet) {
return new CustomerPetResponse(
pet.getCustomerPetId(),
pet.getCustomer().getCustomerId(),
pet.getPetName(),
pet.getSpecies(),
pet.getBreed(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank()
? "/api/v1/my-pets/" + pet.getCustomerPetId() + "/image"
: null,
pet.getCreatedAt(),
pet.getUpdatedAt()
);
}
private void validateImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Please select an image to upload");
}
if (file.getSize() > 5 * 1024 * 1024) {
throw new IllegalArgumentException("Image file size must be less than 5MB");
}
String contentType = file.getContentType();
if (contentType == null) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
}
String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
}
}
private void deleteStoredImageIfPresent(String storedImagePath) {
if (storedImagePath == null || storedImagePath.isBlank()) {
return;
}
try {
catalogImageStorageService.deletePetImage(storedImagePath);
}
catch (IOException ignored) {
}
}
}

View File

@@ -0,0 +1,2 @@
INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice)
VALUES ('Pet Adoption', 'Schedule a visit to meet and adopt an available pet', 30, 0.00);

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS appointment_customer_pet (
appointment_id BIGINT NOT NULL,
customer_pet_id BIGINT NOT NULL,
PRIMARY KEY (appointment_id, customer_pet_id),
FOREIGN KEY (appointment_id) REFERENCES appointment(appointmentId),
FOREIGN KEY (customer_pet_id) REFERENCES customer_pet(customer_pet_id)
);

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS customer_pet (
customer_pet_id BIGINT AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT NOT NULL,
pet_name VARCHAR(50) NOT NULL,
species VARCHAR(50) NOT NULL,
breed VARCHAR(50) NULL,
image_url VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES customer(customerId)
);