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;