Backend correctness fixes

This commit is contained in:
2026-03-10 16:43:46 -06:00
parent c56fb9ab00
commit fad80e436f
21 changed files with 235 additions and 1305 deletions

View File

@@ -16,6 +16,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -125,6 +126,10 @@ public class AuthController {
Map<String, String> error = new HashMap<>();
error.put("message", "Invalid username or password");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
} catch (DisabledException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
}

View File

@@ -40,7 +40,7 @@ public class ChatController {
}
@PostMapping("/conversations")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
@PreAuthorize("hasRole('CUSTOMER')")
public ResponseEntity<ConversationResponse> createConversation(@Valid @RequestBody ConversationRequest request) {
User user = getCurrentUser();
ConversationResponse response = chatService.createConversation(user.getId(), request);

View File

@@ -46,6 +46,7 @@ public class DropdownController {
}
@GetMapping("/customers")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomers() {
return ResponseEntity.ok(
customerRepository.findAll().stream()

View File

@@ -11,6 +11,9 @@ public class AppointmentRequest {
@NotNull(message = "Customer ID is required")
private Long customerId;
@NotNull(message = "Store ID is required")
private Long storeId;
@NotNull(message = "Service ID is required")
private Long serviceId;
@@ -34,6 +37,14 @@ public class AppointmentRequest {
this.customerId = customerId;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public Long getServiceId() {
return serviceId;
}
@@ -80,6 +91,7 @@ public class AppointmentRequest {
if (o == null || getClass() != o.getClass()) return false;
AppointmentRequest that = (AppointmentRequest) o;
return Objects.equals(customerId, that.customerId) &&
Objects.equals(storeId, that.storeId) &&
Objects.equals(serviceId, that.serviceId) &&
Objects.equals(appointmentDate, that.appointmentDate) &&
Objects.equals(appointmentTime, that.appointmentTime) &&
@@ -89,13 +101,14 @@ public class AppointmentRequest {
@Override
public int hashCode() {
return Objects.hash(customerId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds);
return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds);
}
@Override
public String toString() {
return "AppointmentRequest{" +
"customerId=" + customerId +
", storeId=" + storeId +
", serviceId=" + serviceId +
", appointmentDate=" + appointmentDate +
", appointmentTime=" + appointmentTime +

View File

@@ -10,6 +10,8 @@ public class AppointmentResponse {
private Long appointmentId;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
private Long serviceId;
private String serviceName;
private LocalDate appointmentDate;
@@ -23,10 +25,12 @@ public class AppointmentResponse {
public AppointmentResponse() {
}
public AppointmentResponse(Long appointmentId, Long customerId, String customerName, Long serviceId, String serviceName, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, List<String> petNames, List<Long> petIds, LocalDateTime createdAt, LocalDateTime updatedAt) {
public AppointmentResponse(Long appointmentId, Long customerId, String customerName, Long storeId, String storeName, Long serviceId, String serviceName, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, List<String> petNames, List<Long> petIds, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.appointmentId = appointmentId;
this.customerId = customerId;
this.customerName = customerName;
this.storeId = storeId;
this.storeName = storeName;
this.serviceId = serviceId;
this.serviceName = serviceName;
this.appointmentDate = appointmentDate;
@@ -62,6 +66,22 @@ public class AppointmentResponse {
this.customerName = customerName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public Long getServiceId() {
return serviceId;
}
@@ -139,12 +159,12 @@ public class AppointmentResponse {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AppointmentResponse that = (AppointmentResponse) o;
return Objects.equals(appointmentId, that.appointmentId) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petNames, that.petNames) && Objects.equals(petIds, that.petIds) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
return Objects.equals(appointmentId, that.appointmentId) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petNames, that.petNames) && Objects.equals(petIds, that.petIds) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
}
@Override
public int hashCode() {
return Objects.hash(appointmentId, customerId, customerName, serviceId, serviceName, appointmentDate, appointmentTime, appointmentStatus, petNames, petIds, createdAt, updatedAt);
return Objects.hash(appointmentId, customerId, customerName, storeId, storeName, serviceId, serviceName, appointmentDate, appointmentTime, appointmentStatus, petNames, petIds, createdAt, updatedAt);
}
@Override
@@ -153,6 +173,8 @@ public class AppointmentResponse {
"appointmentId=" + appointmentId +
", customerId=" + customerId +
", customerName='" + customerName + '\'' +
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
", serviceId=" + serviceId +
", serviceName='" + serviceName + '\'' +
", appointmentDate=" + appointmentDate +

View File

@@ -23,6 +23,10 @@ public class Appointment {
@JoinColumn(name = "customerId", nullable = false)
private Customer customer;
@ManyToOne
@JoinColumn(name = "storeId", nullable = false)
private StoreLocation store;
@ManyToOne
@JoinColumn(name = "serviceId", nullable = false)
private Service service;
@@ -55,9 +59,10 @@ public class Appointment {
public Appointment() {
}
public Appointment(Long appointmentId, Customer customer, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set<Pet> pets, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set<Pet> pets, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.appointmentId = appointmentId;
this.customer = customer;
this.store = store;
this.service = service;
this.appointmentDate = appointmentDate;
this.appointmentTime = appointmentTime;
@@ -83,6 +88,14 @@ public class Appointment {
this.customer = customer;
}
public StoreLocation getStore() {
return store;
}
public void setStore(StoreLocation store) {
this.store = store;
}
public Service getService() {
return service;
}
@@ -157,6 +170,7 @@ public class Appointment {
return "Appointment{" +
"appointmentId=" + appointmentId +
", customer=" + customer +
", store=" + store +
", service=" + service +
", appointmentDate=" + appointmentDate +
", appointmentTime=" + appointmentTime +

View File

@@ -21,7 +21,7 @@ public class Conversation {
private Long staffId;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)")
private ConversationStatus status = ConversationStatus.OPEN;
@CreationTimestamp

View File

@@ -29,7 +29,7 @@ public class Refund {
private String reason;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private RefundStatus status;
@CreationTimestamp

View File

@@ -18,8 +18,8 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
@Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time")
List<Appointment> findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time);
@Query("SELECT a FROM Appointment a WHERE a.service.serviceId = :serviceId AND a.appointmentDate = :date AND a.appointmentStatus != 'Cancelled'")
List<Appointment> findByServiceAndDate(@Param("serviceId") Long serviceId, @Param("date") LocalDate date);
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'")
List<Appointment> findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date);
@Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " +
"LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +

View File

@@ -0,0 +1,12 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.EmployeeStore;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface EmployeeStoreRepository extends JpaRepository<EmployeeStore, EmployeeStore.EmployeeStoreId> {
Optional<EmployeeStore> findByEmployeeEmployeeId(Long employeeId);
}

View File

@@ -8,6 +8,8 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SaleRepository extends JpaRepository<Sale, Long> {
@@ -16,4 +18,6 @@ public interface SaleRepository extends JpaRepository<Sale, Long> {
"LOWER(s.employee.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.store.storeName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Sale> searchSales(@Param("q") String query, Pageable pageable);
List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
}

View File

@@ -5,6 +5,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.LocalDateTime;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@@ -45,7 +47,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UserDetails userDetails;
try {
userDetails = userDetailsService.loadUserByUsername(username);
} catch (DisabledException ex) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(
"{\"status\":401,\"message\":\"" + ex.getMessage() + "\",\"timestamp\":\"" + LocalDateTime.now() + "\"}"
);
return;
}
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,

View File

@@ -5,14 +5,24 @@ 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.Employee;
import com.petshop.backend.entity.EmployeeStore;
import com.petshop.backend.entity.Pet;
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.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -22,6 +32,7 @@ import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
@@ -32,12 +43,20 @@ public class AppointmentService {
private final CustomerRepository customerRepository;
private final ServiceRepository serviceRepository;
private final PetRepository petRepository;
private final StoreRepository storeRepository;
private final UserRepository userRepository;
private final EmployeeRepository employeeRepository;
private final EmployeeStoreRepository employeeStoreRepository;
public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, ServiceRepository serviceRepository, PetRepository petRepository) {
public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) {
this.appointmentRepository = appointmentRepository;
this.customerRepository = customerRepository;
this.serviceRepository = serviceRepository;
this.petRepository = petRepository;
this.storeRepository = storeRepository;
this.userRepository = userRepository;
this.employeeRepository = employeeRepository;
this.employeeStoreRepository = employeeStoreRepository;
}
public Page<AppointmentResponse> getAllAppointments(String query, Pageable pageable, Long customerId) {
@@ -78,13 +97,20 @@ public class AppointmentService {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
Set<Pet> pets = fetchPets(request.getPetIds());
Appointment appointment = new Appointment();
appointment.setCustomer(customer);
appointment.setStore(store);
appointment.setService(service);
appointment.setAppointmentDate(request.getAppointmentDate());
appointment.setAppointmentTime(request.getAppointmentTime());
@@ -105,12 +131,19 @@ public class AppointmentService {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId())
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId()));
validateStoreAccess(store.getStoreId());
validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
Set<Pet> pets = fetchPets(request.getPetIds());
appointment.setCustomer(customer);
appointment.setStore(store);
appointment.setService(service);
appointment.setAppointmentDate(request.getAppointmentDate());
appointment.setAppointmentTime(request.getAppointmentTime());
@@ -135,21 +168,22 @@ public class AppointmentService {
}
public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
storeRepository.findById(storeId)
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));
List<Appointment> existingAppointments = appointmentRepository.findByServiceAndDate(serviceId, date);
Set<LocalTime> bookedTimes = existingAppointments.stream()
.map(Appointment::getAppointmentTime)
.collect(Collectors.toSet());
List<Appointment> existingAppointments = appointmentRepository.findByStoreAndDate(storeId, date);
List<String> availableSlots = new ArrayList<>();
LocalTime startTime = LocalTime.of(9, 0);
LocalTime endTime = LocalTime.of(17, 0);
LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration());
LocalTime currentTime = startTime;
while (currentTime.isBefore(endTime)) {
if (!bookedTimes.contains(currentTime)) {
while (!currentTime.isAfter(latestStart)) {
if (isSlotAvailable(existingAppointments, service, currentTime, null)) {
availableSlots.add(currentTime.toString());
}
currentTime = currentTime.plusMinutes(30);
@@ -190,6 +224,8 @@ public class AppointmentService {
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(),
@@ -201,4 +237,41 @@ public class AppointmentService {
appointment.getUpdatedAt()
);
}
private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
List<Appointment> existingAppointments = appointmentRepository.findByStoreAndDate(store.getStoreId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
throw new IllegalArgumentException("Appointment time is not available for the selected store and service");
}
}
private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) {
LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration());
for (Appointment existingAppointment : existingAppointments) {
if (appointmentIdToIgnore != null && appointmentIdToIgnore.equals(existingAppointment.getAppointmentId())) {
continue;
}
LocalTime existingStart = existingAppointment.getAppointmentTime();
LocalTime existingEnd = existingStart.plusMinutes(existingAppointment.getService().getServiceDuration());
if (requestedStart.isBefore(existingEnd) && existingStart.isBefore(requestedEnd)) {
return false;
}
}
return true;
}
private void validateStoreAccess(Long requestedStoreId) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
if (user.getRole() != User.Role.STAFF) {
return;
}
Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository);
EmployeeStore employeeStore = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId())
.orElseThrow(() -> new AccessDeniedException("Authenticated staff member is not assigned to a store"));
if (!employeeStore.getStore().getStoreId().equals(requestedStoreId)) {
throw new AccessDeniedException("Staff can only manage appointments for their assigned store");
}
}
}

View File

@@ -43,6 +43,10 @@ public class ChatService {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
if (user.getRole() != User.Role.CUSTOMER) {
throw new AccessDeniedException("Only customers can start new conversations");
}
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
@@ -113,6 +117,18 @@ public class ChatService {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
if (role == User.Role.CUSTOMER) {
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
throw new AccessDeniedException("You can only send messages to your own conversations");
}
} else if (role == User.Role.STAFF) {
if (conversation.getStaffId() != null && !conversation.getStaffId().equals(userId)) {
throw new AccessDeniedException("You can only reply to conversations assigned to you or unassigned conversations");
}
}
Message message = new Message();
message.setConversationId(conversationId);
message.setSenderId(userId);

View File

@@ -26,15 +26,17 @@ public class SaleService {
private final StoreRepository storeRepository;
private final InventoryRepository inventoryRepository;
private final EmployeeRepository employeeRepository;
private final EmployeeStoreRepository employeeStoreRepository;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, UserRepository userRepository, CustomerRepository customerRepository) {
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, UserRepository userRepository, CustomerRepository customerRepository) {
this.saleRepository = saleRepository;
this.productRepository = productRepository;
this.storeRepository = storeRepository;
this.inventoryRepository = inventoryRepository;
this.employeeRepository = employeeRepository;
this.employeeStoreRepository = employeeStoreRepository;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@@ -57,11 +59,20 @@ public class SaleService {
@Transactional
public SaleResponse createSale(SaleRequest request) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository);
Long employeeStoreId = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId())
.orElseThrow(() -> new BusinessException("Authenticated staff member is not assigned to a store"))
.getStore()
.getStoreId();
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
if (user.getRole() == User.Role.STAFF && !employeeStoreId.equals(store.getStoreId())) {
throw new BusinessException("Staff can only create sales for their assigned store");
}
Sale sale = new Sale();
sale.setSaleDate(LocalDateTime.now());
sale.setEmployee(employee);
@@ -100,6 +111,19 @@ public class SaleService {
" for product: " + product.getProdName());
}
int alreadyRefundedQuantity = saleRepository.findByOriginalSaleSaleId(sale.getOriginalSale().getSaleId()).stream()
.flatMap(existingRefund -> existingRefund.getItems().stream())
.filter(existingRefundItem -> existingRefundItem.getProduct().getProdId().equals(itemRequest.getProdId()))
.mapToInt(existingRefundItem -> Math.abs(existingRefundItem.getQuantity()))
.sum();
int refundableQuantity = originalItem.getQuantity() - alreadyRefundedQuantity;
if (itemRequest.getQuantity() > refundableQuantity) {
throw new BusinessException("Refund quantity " + itemRequest.getQuantity() +
" exceeds remaining refundable quantity " + refundableQuantity +
" for product: " + product.getProdName());
}
Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId())
.orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId()));

View File

@@ -20,7 +20,7 @@ spring:
jpa:
hibernate:
ddl-auto: none
ddl-auto: validate
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: ${JPA_SHOW_SQL:false}

View File

@@ -1,205 +0,0 @@
-- Insert Sample Data
INSERT INTO storeLocation (storeName, address, phone, email)
VALUES
('Downtown Branch', '123 Main St', '123-456-7890', 'downtown@petshop.com'),
('North Branch', '456 North Ave', '987-654-3210', 'north@petshop.com'),
('West Side Store', '789 West Blvd', '555-123-4567', 'westside@petshop.com'),
('East End Shop', '321 East Road', '555-987-6543', 'eastend@petshop.com'),
('South Mall Location', '654 South Plaza', '555-246-8135', 'southmall@petshop.com');
INSERT INTO employee (firstName, lastName, email, phone, role, isActive)
VALUES
('John', 'Doe', 'john@petshop.com', '111-222-3333', 'Manager', TRUE),
('Sara', 'Smith', 'sara@petshop.com', '444-555-6666', 'Staff', TRUE),
('Michael', 'Johnson', 'michael@petshop.com', '222-333-4444', 'Groomer', TRUE),
('Lisa', 'Williams', 'lisa@petshop.com', '333-444-5555', 'Staff', TRUE),
('David', 'Brown', 'david@petshop.com', '555-666-7777', 'Veterinarian', TRUE),
('Emma', 'Davis', 'emma@petshop.com', '666-777-8888', 'Manager', FALSE);
INSERT INTO employeeStore (employeeId, storeId)
VALUES
(1, 1),
(2, 1),
(2, 2),
(3, 2),
(4, 3),
(5, 1),
(5, 4),
(6, 5);
INSERT INTO customer (firstName, lastName, email, phone)
VALUES
('Alex', 'Brown', 'alex@gmail.com', '777-888-9999'),
('Emily', 'Clark', 'emily@gmail.com', '666-555-4444'),
('James', 'Wilson', 'james@gmail.com', '888-999-0000'),
('Olivia', 'Martinez', 'olivia@gmail.com', '999-000-1111'),
('William', 'Anderson', 'william@gmail.com', '000-111-2222'),
('Sophia', 'Taylor', 'sophia@gmail.com', '111-222-3333');
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice)
VALUES
('Buddy', 'Dog', 'Labrador', 2, 'Available', 500.00),
('Milo', 'Cat', 'Persian', 1, 'Available', 300.00),
('Charlie', 'Dog', 'Golden Retriever', 3, 'Available', 550.00),
('Luna', 'Cat', 'Siamese', 2, 'Adopted', 350.00),
('Max', 'Dog', 'Beagle', 1, 'Available', 450.00),
('Bella', 'Cat', 'Maine Coon', 4, 'Available', 400.00);
INSERT INTO adoption (petId, customerId, adoptionDate, adoptionStatus)
VALUES
(1, 1, '2026-01-15', 'Completed'),
(4, 3, '2026-01-20', 'Completed'),
(2, 2, '2026-01-25', 'Pending'),
(5, 4, '2026-02-01', 'Completed'),
(6, 5, '2026-02-02', 'Pending');
INSERT INTO supplier (supCompany, supContactFirstName, supContactLastName, supEmail, supPhone)
VALUES
('PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '888-111-2222'),
('Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '888-222-3333'),
('Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '888-333-4444'),
('Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '888-444-5555'),
('Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '888-555-6666');
INSERT INTO category (categoryName, categoryType)
VALUES
('Dog Food', 'Product'),
('Cat Toys', 'Product'),
('Bird Supplies', 'Product'),
('Aquarium', 'Product'),
('Small Animals', 'Product');
INSERT INTO product (prodName, prodPrice, categoryId, prodDesc)
VALUES
('Premium Dog Food', 50.00, 1, 'High quality dog food'),
('Cat Toy Ball', 10.00, 2, 'Colorful toy for cats'),
('Bird Cage Large', 120.00, 3, 'Spacious bird cage'),
('Fish Tank 20 Gallon', 80.00, 4, 'Complete aquarium kit'),
('Hamster Wheel', 15.00, 5, 'Exercise wheel for small pets'),
('Organic Dog Treats', 25.00, 1, 'Natural dog treats');
INSERT INTO productSupplier (supId, prodId, cost)
VALUES
(1, 1, 35.00),
(1, 2, 6.50),
(2, 2, 7.00),
(3, 3, 90.00),
(3, 4, 60.00),
(4, 5, 10.00),
(5, 6, 18.00),
(1, 6, 17.50);
INSERT INTO inventory (prodId, quantity)
VALUES
(1, 100),
(2, 200),
(3, 50),
(4, 30),
(5, 150),
(6, 75);
INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice)
VALUES
('Pet Grooming', 'Full grooming service', 60, 40.00),
('Nail Trimming', 'Quick nail trim', 15, 10.00),
('Bath and Brush', 'Bathing and brushing service', 45, 30.00),
('Veterinary Checkup', 'Complete health examination', 30, 75.00),
('Teeth Cleaning', 'Professional dental cleaning', 90, 100.00);
INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus)
VALUES
(1, 2, '2026-02-01', '10:30:00', 'Booked'),
(2, 1, '2026-02-03', '14:00:00', 'Booked'),
(3, 3, '2026-02-05', '09:00:00', 'Completed'),
(4, 4, '2026-02-07', '11:30:00', 'Booked'),
(5, 5, '2026-02-10', '15:00:00', 'Cancelled');
INSERT INTO appointmentPet (appointmentId, petId)
VALUES
(1, 2),
(2, 1),
(3, 3),
(4, 5),
(5, 6);
INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId)
VALUES
('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1),
('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2),
('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3),
('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1),
('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2),
('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL),
('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4),
('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1, NULL),
('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1),
('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3),
('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL),
('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2),
('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL),
('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1),
('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL),
('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4),
('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL),
('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2),
('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL),
(NOW(), 95.00, 'Card', 1, 1, 1);
INSERT INTO saleItem (saleId, prodId, quantity, unitPrice)
VALUES
(1, 1, 2, 50.00),
(1, 6, 1, 25.00),
(2, 3, 1, 120.00),
(2, 4, 1, 80.00),
(3, 2, 3, 10.00),
(3, 5, 2, 15.00),
(4, 1, 3, 50.00),
(5, 4, 1, 80.00),
(6, 2, 4, 10.00),
(6, 5, 1, 15.00),
(6, 6, 1, 25.00),
(6, 1, 1, 50.00),
(7, 3, 2, 120.00),
(8, 1, 1, 50.00),
(8, 2, 3, 10.00),
(9, 1, 3, 50.00),
(9, 6, 1, 25.00),
(10, 3, 1, 120.00),
(11, 5, 1, 15.00),
(11, 2, 3, 10.00),
(12, 4, 2, 80.00),
(13, 6, 4, 25.00),
(14, 1, 1, 50.00),
(15, 2, 2, 10.00),
(15, 5, 1, 15.00),
(15, 6, 2, 25.00),
(16, 3, 1, 120.00),
(16, 4, 1, 80.00),
(17, 4, 1, 80.00),
(17, 1, 1, 50.00),
(17, 6, 1, 25.00),
(18, 6, 2, 25.00),
(18, 2, 2, 10.00),
(18, 5, 1, 15.00),
(19, 1, 2, 50.00),
(19, 6, 2, 25.00),
(20, 2, 5, 10.00),
(20, 5, 3, 15.00);
INSERT INTO purchaseOrder (supId, orderDate, status)
VALUES
(1, '2025-01-15', 'Delivered'),
(2, '2025-01-20', 'Pending'),
(3, '2025-02-01', 'Delivered'),
(4, '2025-02-10', 'In Transit'),
(1, '2025-02-15', 'Pending');
INSERT INTO activityLog (employeeId, activity)
VALUES
(1, 'Created new sale'),
(2, 'Booked appointment'),
(3, 'Completed grooming service'),
(4, 'Processed inventory order'),
(5, 'Conducted health checkup'),
(1, 'Updated customer information');

View File

@@ -0,0 +1,19 @@
ALTER TABLE appointment
ADD COLUMN storeId BIGINT NULL AFTER customerId;
UPDATE appointment
SET storeId = 1
WHERE storeId IS NULL;
ALTER TABLE appointment
MODIFY COLUMN storeId BIGINT NOT NULL,
ADD CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId);
DELETE es1
FROM employeeStore es1
JOIN employeeStore es2
ON es1.employeeId = es2.employeeId
AND es1.storeId > es2.storeId;
ALTER TABLE employeeStore
ADD CONSTRAINT uk_employeeStore_employee UNIQUE (employeeId);

View File

@@ -1,250 +0,0 @@
-- Create Tables
CREATE TABLE IF NOT EXISTS storeLocation (
storeId BIGINT AUTO_INCREMENT PRIMARY KEY,
storeName VARCHAR(100) NOT NULL,
address VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS employee (
employeeId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
role VARCHAR(50) NOT NULL,
isActive BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_employee_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS employeeStore (
employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL,
PRIMARY KEY (employeeId, storeId),
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId)
);
CREATE TABLE IF NOT EXISTS customer (
customerId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_customer_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS pet (
petId BIGINT AUTO_INCREMENT PRIMARY KEY,
petName VARCHAR(50) NOT NULL,
petSpecies VARCHAR(50) NOT NULL,
petBreed VARCHAR(50) NOT NULL,
petAge INT NOT NULL,
petStatus VARCHAR(20) NOT NULL,
petPrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS adoption (
adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY,
petId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
adoptionDate DATE NOT NULL,
adoptionStatus VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (petId) REFERENCES pet(petId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS supplier (
supId BIGINT AUTO_INCREMENT PRIMARY KEY,
supCompany VARCHAR(100) NOT NULL,
supContactFirstName VARCHAR(50) NOT NULL,
supContactLastName VARCHAR(50) NOT NULL,
supEmail VARCHAR(100) NOT NULL,
supPhone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS category (
categoryId BIGINT AUTO_INCREMENT PRIMARY KEY,
categoryName VARCHAR(100) NOT NULL,
categoryType VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product (
prodId BIGINT AUTO_INCREMENT PRIMARY KEY,
prodName VARCHAR(100) NOT NULL,
prodPrice DECIMAL(10, 2) NOT NULL,
categoryId BIGINT NOT NULL,
prodDesc TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (categoryId) REFERENCES category(categoryId)
);
CREATE TABLE IF NOT EXISTS productSupplier (
supId BIGINT NOT NULL,
prodId BIGINT NOT NULL,
cost DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (supId, prodId),
FOREIGN KEY (supId) REFERENCES supplier(supId),
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS inventory (
inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY,
prodId BIGINT NOT NULL,
quantity INT DEFAULT 0 NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS service (
serviceId BIGINT AUTO_INCREMENT PRIMARY KEY,
serviceName VARCHAR(100) NOT NULL,
serviceDesc TEXT,
serviceDuration INT NOT NULL,
servicePrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS appointment (
appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY,
serviceId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
appointmentDate DATE NOT NULL,
appointmentTime TIME NOT NULL,
appointmentStatus VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (serviceId) REFERENCES service(serviceId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS appointmentPet (
appointmentId BIGINT NOT NULL,
petId BIGINT NOT NULL,
PRIMARY KEY (appointmentId, petId),
FOREIGN KEY (appointmentId) REFERENCES appointment(appointmentId),
FOREIGN KEY (petId) REFERENCES pet(petId)
);
CREATE TABLE IF NOT EXISTS sale (
saleId BIGINT AUTO_INCREMENT PRIMARY KEY,
saleDate DATETIME NOT NULL,
totalAmount DECIMAL(10, 2) NOT NULL,
paymentMethod VARCHAR(50) NOT NULL,
employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL,
customerId BIGINT NULL,
isRefund BOOLEAN DEFAULT FALSE NOT NULL,
originalSaleId BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (originalSaleId) REFERENCES sale(saleId)
);
CREATE TABLE IF NOT EXISTS saleItem (
saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY,
saleId BIGINT NOT NULL,
prodId BIGINT NOT NULL,
quantity INT NOT NULL,
unitPrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (saleId) REFERENCES sale(saleId),
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS purchaseOrder (
purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY,
supId BIGINT NOT NULL,
orderDate DATE NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (supId) REFERENCES supplier(supId)
);
CREATE TABLE IF NOT EXISTS activityLog (
logId BIGINT AUTO_INCREMENT PRIMARY KEY,
employeeId BIGINT NOT NULL,
activity TEXT NOT NULL,
logTimestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId)
);
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
fullName VARCHAR(100),
avatarUrl VARCHAR(255),
role VARCHAR(20) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS refund (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
saleId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
reason VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (saleId) REFERENCES sale(saleId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS conversation (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customerId BIGINT NOT NULL,
staffId BIGINT,
status VARCHAR(20) DEFAULT 'OPEN',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (staffId) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversationId BIGINT NOT NULL,
senderId BIGINT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
isRead BOOLEAN DEFAULT FALSE,
FOREIGN KEY (conversationId) REFERENCES conversation(id),
FOREIGN KEY (senderId) REFERENCES users(id)
);
-- Add foreign keys for user_id linkage
ALTER TABLE employee ADD CONSTRAINT fk_employee_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE customer ADD CONSTRAINT fk_customer_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;