WIP: Update morefiles #152
@@ -36,4 +36,7 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
|
||||
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
|
||||
Page<Appointment> searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
|
||||
|
||||
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'")
|
||||
List<Appointment> findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date);
|
||||
}
|
||||
|
||||
@@ -110,14 +110,10 @@ public class AppointmentService {
|
||||
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -125,6 +121,9 @@ public class AppointmentService {
|
||||
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
|
||||
Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||
|
||||
Appointment appointment = new Appointment();
|
||||
appointment.setCustomer(customer);
|
||||
appointment.setStore(store);
|
||||
@@ -158,14 +157,10 @@ public class AppointmentService {
|
||||
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -173,6 +168,9 @@ public class AppointmentService {
|
||||
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
|
||||
Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
||||
|
||||
appointment.setCustomer(customer);
|
||||
appointment.setStore(store);
|
||||
appointment.setService(service);
|
||||
@@ -201,7 +199,6 @@ public class AppointmentService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
|
||||
storeRepository.findById(storeId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
|
||||
@@ -209,16 +206,10 @@ public class AppointmentService {
|
||||
com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// CHANGED: filter by serviceId too
|
||||
List<Appointment> existingAppointments = appointmentRepository
|
||||
.findByStoreAndDate(storeId, date)
|
||||
.stream()
|
||||
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||
List<Employee> assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream()
|
||||
.filter(es -> isAssignableEmployee(es.getEmployee()))
|
||||
.map(EmployeeStore::getEmployee)
|
||||
.collect(Collectors.toList());
|
||||
// -------------------------------------------------------
|
||||
|
||||
|
||||
List<String> availableSlots = new ArrayList<>();
|
||||
LocalTime startTime = LocalTime.of(9, 0);
|
||||
@@ -227,7 +218,13 @@ public class AppointmentService {
|
||||
|
||||
LocalTime currentTime = startTime;
|
||||
while (!currentTime.isAfter(latestStart)) {
|
||||
if (isSlotAvailable(existingAppointments, service, currentTime, null)) {
|
||||
final LocalTime slotTime = currentTime;
|
||||
boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> {
|
||||
List<Appointment> empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date);
|
||||
return isSlotAvailable(empAppointments, service, slotTime, null);
|
||||
});
|
||||
|
||||
if (anyEmployeeAvailable) {
|
||||
availableSlots.add(currentTime.toString());
|
||||
}
|
||||
currentTime = currentTime.plusMinutes(30);
|
||||
@@ -343,15 +340,11 @@ public class AppointmentService {
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
|
||||
// Filter by same service only - different services can run at same time
|
||||
private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
|
||||
List<Appointment> existingAppointments = appointmentRepository
|
||||
.findByStoreAndDate(store.getStoreId(), date)
|
||||
.stream()
|
||||
.filter(a -> a.getService().getServiceId().equals(service.getServiceId()))
|
||||
.collect(Collectors.toList());
|
||||
.findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date);
|
||||
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
|
||||
throw new IllegalArgumentException("Appointment time is not available for the selected store and service");
|
||||
throw new IllegalArgumentException("The selected employee is already booked for this time slot");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet
|
||||
|
||||
-- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet
|
||||
-- Note: pet species and breed might be null in pet table, but we copy them over if present
|
||||
INSERT INTO customer_pet (customer_id, pet_name, species, breed)
|
||||
SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed
|
||||
FROM appointmentPet ap
|
||||
JOIN appointment a ON a.appointmentId = ap.appointmentId
|
||||
JOIN pet p ON p.petId = ap.petId
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM customer_pet cp
|
||||
WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName
|
||||
);
|
||||
|
||||
-- Step 2: Link the appointment to the customer_pet
|
||||
INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id)
|
||||
SELECT ap.appointmentId, cp.customer_pet_id
|
||||
FROM appointmentPet ap
|
||||
JOIN appointment a ON a.appointmentId = ap.appointmentId
|
||||
JOIN pet p ON p.petId = ap.petId
|
||||
JOIN customer_pet cp ON cp.customer_id = a.customerId AND cp.pet_name = p.petName
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM appointment_customer_pet acp
|
||||
WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id
|
||||
);
|
||||
|
||||
-- Step 3: Remove the old legacy relationships so it strictly uses the new ones
|
||||
DELETE FROM appointmentPet;
|
||||
@@ -130,29 +130,34 @@ class AppointmentServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAvailabilityAllowsDifferentServicesAtSameTime() {
|
||||
void checkAvailabilityAllowsConcurrentAppointmentsIfAnotherEmployeeFree() {
|
||||
Employee employee2 = new Employee();
|
||||
employee2.setEmployeeId(8L);
|
||||
employee2.setUserId(8L);
|
||||
employee2.setFirstName("Bob");
|
||||
employee2.setIsActive(true);
|
||||
|
||||
User staffUser2 = new User();
|
||||
staffUser2.setId(8L);
|
||||
staffUser2.setRole(User.Role.STAFF);
|
||||
staffUser2.setActive(true);
|
||||
when(userRepository.findById(8L)).thenReturn(Optional.of(staffUser2));
|
||||
|
||||
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
|
||||
|
||||
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
|
||||
.thenReturn(List.of(new EmployeeStore(employee, store), new EmployeeStore(employee2, store)));
|
||||
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of(existing));
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(8L, date)).thenReturn(List.of());
|
||||
|
||||
List<String> slots = appointmentService.checkAvailability(1L, 2L, date);
|
||||
|
||||
assertTrue(slots.contains("10:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAvailabilityBlocksSameServiceAtSameTime() {
|
||||
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
|
||||
|
||||
List<String> slots = appointmentService.checkAvailability(1L, 1L, date);
|
||||
|
||||
assertFalse(slots.contains("10:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() {
|
||||
setAuthentication(7L, User.Role.STAFF);
|
||||
@@ -172,7 +177,7 @@ class AppointmentServiceTest {
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
|
||||
.thenReturn(List.of(new EmployeeStore(employee, store)));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
|
||||
when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet));
|
||||
|
||||
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
|
||||
@@ -204,7 +209,7 @@ class AppointmentServiceTest {
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
|
||||
.thenReturn(List.of(new EmployeeStore(employee, store)));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
|
||||
when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet));
|
||||
|
||||
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
|
||||
@@ -245,7 +250,7 @@ class AppointmentServiceTest {
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
|
||||
when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet));
|
||||
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
|
||||
.thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store)));
|
||||
@@ -282,7 +287,7 @@ class AppointmentServiceTest {
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
|
||||
when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of());
|
||||
when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet));
|
||||
when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L))
|
||||
.thenReturn(List.of(new EmployeeStore(employee, store)));
|
||||
|
||||
Reference in New Issue
Block a user