From 8b10696c841e8d61ec248dabcae0caff780e756f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 10 Mar 2026 13:03:14 -0600 Subject: [PATCH] Fix backend security, validation, and API contracts --- docker-compose.dev.yml | 2 - docker-compose.yml | 2 - pom.xml | 10 + .../backend/controller/SaleController.java | 2 + .../dto/adoption/AdoptionResponse.java | 18 +- .../repository/ConversationRepository.java | 1 + .../backend/security/SecurityConfig.java | 2 +- .../security/UserDetailsServiceImpl.java | 5 + .../backend/service/AdoptionService.java | 1 + .../backend/service/AppointmentService.java | 14 + .../petshop/backend/service/ChatService.java | 16 +- .../petshop/backend/service/SaleService.java | 75 ++++-- src/main/resources/application.yml | 7 +- .../db/migration/V1__baseline_schema.sql | 250 ++++++++++++++++++ 14 files changed, 373 insertions(+), 32 deletions(-) create mode 100644 src/main/resources/db/migration/V1__baseline_schema.sql diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8e21e0e5..774e5393 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,8 +12,6 @@ services: - "3306:3306" volumes: - db_data:/var/lib/mysql - - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql - - ./src/main/resources/data.sql:/docker-entrypoint-initdb.d/02-data.sql healthcheck: test: ["CMD", "mysql", "-uroot", "-proot", "-e", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Petstoredb' AND table_name='users';"] interval: 10s diff --git a/docker-compose.yml b/docker-compose.yml index 423dab50..1966e7e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,6 @@ services: - "3306:3306" volumes: - db_data:/var/lib/mysql - - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql - - ./src/main/resources/data.sql:/docker-entrypoint-initdb.d/02-data.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] interval: 5s diff --git a/pom.xml b/pom.xml index fae96e01..1803f3ec 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,16 @@ runtime + + org.flywaydb + flyway-core + + + + org.flywaydb + flyway-mysql + + io.jsonwebtoken jjwt-api diff --git a/src/main/java/com/petshop/backend/controller/SaleController.java b/src/main/java/com/petshop/backend/controller/SaleController.java index d7e165fe..5d29f80d 100644 --- a/src/main/java/com/petshop/backend/controller/SaleController.java +++ b/src/main/java/com/petshop/backend/controller/SaleController.java @@ -22,6 +22,7 @@ public class SaleController { } @GetMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAllSales( @RequestParam(required = false) String q, Pageable pageable) { @@ -29,6 +30,7 @@ public class SaleController { } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity getSaleById(@PathVariable Long id) { return ResponseEntity.ok(saleService.getSaleById(id)); } diff --git a/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java b/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java index d26e88a1..6f2d0556 100644 --- a/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java +++ b/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.adoption; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Objects; @@ -12,13 +13,14 @@ public class AdoptionResponse { private String customerName; private LocalDate adoptionDate; private String adoptionStatus; + private BigDecimal adoptionFee; private LocalDateTime createdAt; private LocalDateTime updatedAt; public AdoptionResponse() { } - public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { + public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.petId = petId; this.petName = petName; @@ -26,6 +28,7 @@ public class AdoptionResponse { this.customerName = customerName; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; + this.adoptionFee = adoptionFee; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -86,6 +89,14 @@ public class AdoptionResponse { this.adoptionStatus = adoptionStatus; } + public BigDecimal getAdoptionFee() { + return adoptionFee; + } + + public void setAdoptionFee(BigDecimal adoptionFee) { + this.adoptionFee = adoptionFee; + } + public LocalDateTime getCreatedAt() { return createdAt; } @@ -107,12 +118,12 @@ public class AdoptionResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AdoptionResponse that = (AdoptionResponse) o; - return Objects.equals(adoptionId, that.adoptionId) && Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(adoptionId, that.adoptionId) && Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && Objects.equals(adoptionFee, that.adoptionFee) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(adoptionId, petId, petName, customerId, customerName, adoptionDate, adoptionStatus, createdAt, updatedAt); + return Objects.hash(adoptionId, petId, petName, customerId, customerName, adoptionDate, adoptionStatus, adoptionFee, createdAt, updatedAt); } @Override @@ -125,6 +136,7 @@ public class AdoptionResponse { ", customerName='" + customerName + '\'' + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + + ", adoptionFee=" + adoptionFee + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; diff --git a/src/main/java/com/petshop/backend/repository/ConversationRepository.java b/src/main/java/com/petshop/backend/repository/ConversationRepository.java index 142853d6..98d457b8 100644 --- a/src/main/java/com/petshop/backend/repository/ConversationRepository.java +++ b/src/main/java/com/petshop/backend/repository/ConversationRepository.java @@ -10,4 +10,5 @@ import java.util.List; public interface ConversationRepository extends JpaRepository { List findByCustomerId(Long customerId); List findByStaffId(Long staffId); + List findByStaffIdIsNull(); } diff --git a/src/main/java/com/petshop/backend/security/SecurityConfig.java b/src/main/java/com/petshop/backend/security/SecurityConfig.java index f4293aa7..9841d9fd 100644 --- a/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -41,9 +41,9 @@ public class SecurityConfig { .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll() - .requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java b/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java index 06c3870b..f6956615 100644 --- a/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java +++ b/src/main/java/com/petshop/backend/security/UserDetailsServiceImpl.java @@ -2,6 +2,7 @@ package com.petshop.backend.security; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; +import org.springframework.security.authentication.DisabledException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -24,6 +25,10 @@ public class UserDetailsServiceImpl implements UserDetailsService { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + if (user.getActive() == null || !user.getActive()) { + throw new DisabledException("User account is inactive"); + } + return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), diff --git a/src/main/java/com/petshop/backend/service/AdoptionService.java b/src/main/java/com/petshop/backend/service/AdoptionService.java index b53c683f..a8f7f476 100644 --- a/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -119,6 +119,7 @@ public class AdoptionService { adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), adoption.getAdoptionDate(), adoption.getAdoptionStatus(), + adoption.getPet().getPetPrice(), adoption.getCreatedAt(), adoption.getUpdatedAt() ); diff --git a/src/main/java/com/petshop/backend/service/AppointmentService.java b/src/main/java/com/petshop/backend/service/AppointmentService.java index ff90a339..6b9d3548 100644 --- a/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.HashSet; @@ -72,6 +73,8 @@ public class AppointmentService { @Transactional public AppointmentResponse createAppointment(AppointmentRequest request) { + validateAppointmentRequest(request); + Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); @@ -94,6 +97,8 @@ public class AppointmentService { @Transactional public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { + validateAppointmentRequest(request); + Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); @@ -153,6 +158,15 @@ public class AppointmentService { return availableSlots; } + private void validateAppointmentRequest(AppointmentRequest request) { + if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { + LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); + if (appointmentDateTime.isBefore(LocalDateTime.now())) { + throw new IllegalArgumentException("Booked appointments must be scheduled in the future"); + } + } + } + private Set fetchPets(List petIds) { Set pets = new HashSet<>(); for (Long petId : petIds) { diff --git a/src/main/java/com/petshop/backend/service/ChatService.java b/src/main/java/com/petshop/backend/service/ChatService.java index 2f7b9d6a..5842f6e7 100644 --- a/src/main/java/com/petshop/backend/service/ChatService.java +++ b/src/main/java/com/petshop/backend/service/ChatService.java @@ -69,10 +69,10 @@ public class ChatService { .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); conversations = conversationRepository.findByCustomerId(customer.getCustomerId()); } else if (role == User.Role.STAFF) { - conversations = conversationRepository.findByStaffId(userId); - if (conversations.isEmpty()) { - conversations = conversationRepository.findAll(); - } + List assignedToMe = conversationRepository.findByStaffId(userId); + List unassigned = conversationRepository.findByStaffIdIsNull(); + conversations = new java.util.ArrayList<>(assignedToMe); + conversations.addAll(unassigned); } else { conversations = conversationRepository.findAll(); } @@ -96,6 +96,10 @@ public class ChatService { if (!conversation.getCustomerId().equals(customer.getCustomerId())) { throw new AccessDeniedException("You can only view your own conversations"); } + } else if (role == User.Role.STAFF) { + if (conversation.getStaffId() != null && !conversation.getStaffId().equals(userId)) { + throw new AccessDeniedException("You can only view conversations assigned to you or unassigned conversations"); + } } List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); @@ -134,6 +138,10 @@ public class ChatService { if (!conversation.getCustomerId().equals(customer.getCustomerId())) { throw new AccessDeniedException("You can only view messages from your own conversations"); } + } else if (role == User.Role.STAFF) { + if (conversation.getStaffId() != null && !conversation.getStaffId().equals(userId)) { + throw new AccessDeniedException("You can only view messages from conversations assigned to you or unassigned conversations"); + } } List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); diff --git a/src/main/java/com/petshop/backend/service/SaleService.java b/src/main/java/com/petshop/backend/service/SaleService.java index e0459d3f..fab3e6da 100644 --- a/src/main/java/com/petshop/backend/service/SaleService.java +++ b/src/main/java/com/petshop/backend/service/SaleService.java @@ -84,32 +84,69 @@ public class SaleService { BigDecimal totalAmount = BigDecimal.ZERO; List saleItems = new ArrayList<>(); - for (var itemRequest : request.getItems()) { - Product product = productRepository.findById(itemRequest.getProdId()) - .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); + if (sale.getIsRefund() && sale.getOriginalSale() != null) { + for (var itemRequest : request.getItems()) { + Product product = productRepository.findById(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); - Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) - .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + SaleItem originalItem = sale.getOriginalSale().getItems().stream() + .filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId())) + .findFirst() + .orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in the original sale")); - if (inventory.getQuantity() < itemRequest.getQuantity()) { - throw new BusinessException("Insufficient stock for product: " + product.getProdName() + - ". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity()); + if (itemRequest.getQuantity() > originalItem.getQuantity()) { + throw new BusinessException("Refund quantity " + itemRequest.getQuantity() + + " exceeds original quantity " + originalItem.getQuantity() + + " for product: " + product.getProdName()); + } + + Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + + inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity()); + inventoryRepository.save(inventory); + + BigDecimal unitPrice = originalItem.getUnitPrice(); + BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + SaleItem saleItem = new SaleItem(); + saleItem.setSale(sale); + saleItem.setProduct(product); + saleItem.setQuantity(-itemRequest.getQuantity()); + saleItem.setUnitPrice(unitPrice); + + saleItems.add(saleItem); + totalAmount = totalAmount.add(itemTotal); } + totalAmount = totalAmount.negate(); + } else { + for (var itemRequest : request.getItems()) { + Product product = productRepository.findById(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); - inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity()); - inventoryRepository.save(inventory); + Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); - BigDecimal unitPrice = product.getProdPrice(); - BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + if (inventory.getQuantity() < itemRequest.getQuantity()) { + throw new BusinessException("Insufficient stock for product: " + product.getProdName() + + ". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity()); + } - SaleItem saleItem = new SaleItem(); - saleItem.setSale(sale); - saleItem.setProduct(product); - saleItem.setQuantity(itemRequest.getQuantity()); - saleItem.setUnitPrice(unitPrice); + inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity()); + inventoryRepository.save(inventory); - saleItems.add(saleItem); - totalAmount = totalAmount.add(itemTotal); + BigDecimal unitPrice = product.getProdPrice(); + BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())); + + SaleItem saleItem = new SaleItem(); + saleItem.setSale(sale); + saleItem.setProduct(product); + saleItem.setQuantity(itemRequest.getQuantity()); + saleItem.setUnitPrice(unitPrice); + + saleItems.add(saleItem); + totalAmount = totalAmount.add(itemTotal); + } } sale.setTotalAmount(totalAmount); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 40e85de4..6338af51 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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} @@ -29,6 +29,11 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQLDialect + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + server: port: ${SERVER_PORT:8080} servlet: diff --git a/src/main/resources/db/migration/V1__baseline_schema.sql b/src/main/resources/db/migration/V1__baseline_schema.sql new file mode 100644 index 00000000..097a7328 --- /dev/null +++ b/src/main/resources/db/migration/V1__baseline_schema.sql @@ -0,0 +1,250 @@ +-- 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;