Fix backend security, validation, and API contracts

This commit is contained in:
2026-03-10 13:03:14 -06:00
parent e6e44f70d8
commit 8b10696c84
14 changed files with 373 additions and 32 deletions

View File

@@ -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

View File

@@ -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

10
pom.xml
View File

@@ -54,6 +54,16 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>

View File

@@ -22,6 +22,7 @@ public class SaleController {
}
@GetMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Page<SaleResponse>> getAllSales(
@RequestParam(required = false) String q,
Pageable pageable) {
@@ -29,6 +30,7 @@ public class SaleController {
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<SaleResponse> getSaleById(@PathVariable Long id) {
return ResponseEntity.ok(saleService.getSaleById(id));
}

View File

@@ -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 +
'}';

View File

@@ -10,4 +10,5 @@ import java.util.List;
public interface ConversationRepository extends JpaRepository<Conversation, Long> {
List<Conversation> findByCustomerId(Long customerId);
List<Conversation> findByStaffId(Long staffId);
List<Conversation> findByStaffIdIsNull();
}

View File

@@ -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))

View File

@@ -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(),

View File

@@ -119,6 +119,7 @@ public class AdoptionService {
adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(),
adoption.getAdoptionDate(),
adoption.getAdoptionStatus(),
adoption.getPet().getPetPrice(),
adoption.getCreatedAt(),
adoption.getUpdatedAt()
);

View File

@@ -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<Pet> fetchPets(List<Long> petIds) {
Set<Pet> pets = new HashSet<>();
for (Long petId : petIds) {

View File

@@ -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<Conversation> assignedToMe = conversationRepository.findByStaffId(userId);
List<Conversation> 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<Message> 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<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);

View File

@@ -84,32 +84,69 @@ public class SaleService {
BigDecimal totalAmount = BigDecimal.ZERO;
List<SaleItem> 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);

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}
@@ -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:

View File

@@ -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;