Fix backend security, validation, and API contracts
This commit is contained in:
@@ -12,8 +12,6 @@ services:
|
|||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- 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:
|
healthcheck:
|
||||||
test: ["CMD", "mysql", "-uroot", "-proot", "-e", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Petstoredb' AND table_name='users';"]
|
test: ["CMD", "mysql", "-uroot", "-proot", "-e", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Petstoredb' AND table_name='users';"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ services:
|
|||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- 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:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
10
pom.xml
10
pom.xml
@@ -54,6 +54,16 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-mysql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-api</artifactId>
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
public ResponseEntity<Page<SaleResponse>> getAllSales(
|
public ResponseEntity<Page<SaleResponse>> getAllSales(
|
||||||
@RequestParam(required = false) String q,
|
@RequestParam(required = false) String q,
|
||||||
Pageable pageable) {
|
Pageable pageable) {
|
||||||
@@ -29,6 +30,7 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
public ResponseEntity<SaleResponse> getSaleById(@PathVariable Long id) {
|
public ResponseEntity<SaleResponse> getSaleById(@PathVariable Long id) {
|
||||||
return ResponseEntity.ok(saleService.getSaleById(id));
|
return ResponseEntity.ok(saleService.getSaleById(id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.petshop.backend.dto.adoption;
|
package com.petshop.backend.dto.adoption;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -12,13 +13,14 @@ public class AdoptionResponse {
|
|||||||
private String customerName;
|
private String customerName;
|
||||||
private LocalDate adoptionDate;
|
private LocalDate adoptionDate;
|
||||||
private String adoptionStatus;
|
private String adoptionStatus;
|
||||||
|
private BigDecimal adoptionFee;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public AdoptionResponse() {
|
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.adoptionId = adoptionId;
|
||||||
this.petId = petId;
|
this.petId = petId;
|
||||||
this.petName = petName;
|
this.petName = petName;
|
||||||
@@ -26,6 +28,7 @@ public class AdoptionResponse {
|
|||||||
this.customerName = customerName;
|
this.customerName = customerName;
|
||||||
this.adoptionDate = adoptionDate;
|
this.adoptionDate = adoptionDate;
|
||||||
this.adoptionStatus = adoptionStatus;
|
this.adoptionStatus = adoptionStatus;
|
||||||
|
this.adoptionFee = adoptionFee;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -86,6 +89,14 @@ public class AdoptionResponse {
|
|||||||
this.adoptionStatus = adoptionStatus;
|
this.adoptionStatus = adoptionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getAdoptionFee() {
|
||||||
|
return adoptionFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdoptionFee(BigDecimal adoptionFee) {
|
||||||
|
this.adoptionFee = adoptionFee;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -107,12 +118,12 @@ public class AdoptionResponse {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
AdoptionResponse that = (AdoptionResponse) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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
|
@Override
|
||||||
@@ -125,6 +136,7 @@ public class AdoptionResponse {
|
|||||||
", customerName='" + customerName + '\'' +
|
", customerName='" + customerName + '\'' +
|
||||||
", adoptionDate=" + adoptionDate +
|
", adoptionDate=" + adoptionDate +
|
||||||
", adoptionStatus='" + adoptionStatus + '\'' +
|
", adoptionStatus='" + adoptionStatus + '\'' +
|
||||||
|
", adoptionFee=" + adoptionFee +
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ import java.util.List;
|
|||||||
public interface ConversationRepository extends JpaRepository<Conversation, Long> {
|
public interface ConversationRepository extends JpaRepository<Conversation, Long> {
|
||||||
List<Conversation> findByCustomerId(Long customerId);
|
List<Conversation> findByCustomerId(Long customerId);
|
||||||
List<Conversation> findByStaffId(Long staffId);
|
List<Conversation> findByStaffId(Long staffId);
|
||||||
|
List<Conversation> findByStaffIdIsNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").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/services/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.petshop.backend.security;
|
|||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
@@ -24,6 +25,10 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
|||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + 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(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ public class AdoptionService {
|
|||||||
adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(),
|
adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(),
|
||||||
adoption.getAdoptionDate(),
|
adoption.getAdoptionDate(),
|
||||||
adoption.getAdoptionStatus(),
|
adoption.getAdoptionStatus(),
|
||||||
|
adoption.getPet().getPetPrice(),
|
||||||
adoption.getCreatedAt(),
|
adoption.getCreatedAt(),
|
||||||
adoption.getUpdatedAt()
|
adoption.getUpdatedAt()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -72,6 +73,8 @@ public class AppointmentService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public AppointmentResponse createAppointment(AppointmentRequest request) {
|
public AppointmentResponse createAppointment(AppointmentRequest request) {
|
||||||
|
validateAppointmentRequest(request);
|
||||||
|
|
||||||
Customer customer = customerRepository.findById(request.getCustomerId())
|
Customer customer = customerRepository.findById(request.getCustomerId())
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
|
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
|
||||||
|
|
||||||
@@ -94,6 +97,8 @@ public class AppointmentService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) {
|
public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) {
|
||||||
|
validateAppointmentRequest(request);
|
||||||
|
|
||||||
Appointment appointment = appointmentRepository.findById(id)
|
Appointment appointment = appointmentRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id));
|
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id));
|
||||||
|
|
||||||
@@ -153,6 +158,15 @@ public class AppointmentService {
|
|||||||
return availableSlots;
|
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) {
|
private Set<Pet> fetchPets(List<Long> petIds) {
|
||||||
Set<Pet> pets = new HashSet<>();
|
Set<Pet> pets = new HashSet<>();
|
||||||
for (Long petId : petIds) {
|
for (Long petId : petIds) {
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ public class ChatService {
|
|||||||
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
|
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
|
||||||
conversations = conversationRepository.findByCustomerId(customer.getCustomerId());
|
conversations = conversationRepository.findByCustomerId(customer.getCustomerId());
|
||||||
} else if (role == User.Role.STAFF) {
|
} else if (role == User.Role.STAFF) {
|
||||||
conversations = conversationRepository.findByStaffId(userId);
|
List<Conversation> assignedToMe = conversationRepository.findByStaffId(userId);
|
||||||
if (conversations.isEmpty()) {
|
List<Conversation> unassigned = conversationRepository.findByStaffIdIsNull();
|
||||||
conversations = conversationRepository.findAll();
|
conversations = new java.util.ArrayList<>(assignedToMe);
|
||||||
}
|
conversations.addAll(unassigned);
|
||||||
} else {
|
} else {
|
||||||
conversations = conversationRepository.findAll();
|
conversations = conversationRepository.findAll();
|
||||||
}
|
}
|
||||||
@@ -96,6 +96,10 @@ public class ChatService {
|
|||||||
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
|
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
|
||||||
throw new AccessDeniedException("You can only view your own conversations");
|
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);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
@@ -134,6 +138,10 @@ public class ChatService {
|
|||||||
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
|
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
|
||||||
throw new AccessDeniedException("You can only view messages from your own conversations");
|
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);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
|
|||||||
@@ -84,32 +84,69 @@ public class SaleService {
|
|||||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||||
List<SaleItem> saleItems = new ArrayList<>();
|
List<SaleItem> saleItems = new ArrayList<>();
|
||||||
|
|
||||||
for (var itemRequest : request.getItems()) {
|
if (sale.getIsRefund() && sale.getOriginalSale() != null) {
|
||||||
Product product = productRepository.findById(itemRequest.getProdId())
|
for (var itemRequest : request.getItems()) {
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId()));
|
Product product = productRepository.findById(itemRequest.getProdId())
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId()));
|
||||||
|
|
||||||
Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId())
|
SaleItem originalItem = sale.getOriginalSale().getItems().stream()
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId()));
|
.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()) {
|
if (itemRequest.getQuantity() > originalItem.getQuantity()) {
|
||||||
throw new BusinessException("Insufficient stock for product: " + product.getProdName() +
|
throw new BusinessException("Refund quantity " + itemRequest.getQuantity() +
|
||||||
". Available: " + inventory.getQuantity() + ", requested: " + 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());
|
Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId())
|
||||||
inventoryRepository.save(inventory);
|
.orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId()));
|
||||||
|
|
||||||
BigDecimal unitPrice = product.getProdPrice();
|
if (inventory.getQuantity() < itemRequest.getQuantity()) {
|
||||||
BigDecimal itemTotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity()));
|
throw new BusinessException("Insufficient stock for product: " + product.getProdName() +
|
||||||
|
". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity());
|
||||||
|
}
|
||||||
|
|
||||||
SaleItem saleItem = new SaleItem();
|
inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity());
|
||||||
saleItem.setSale(sale);
|
inventoryRepository.save(inventory);
|
||||||
saleItem.setProduct(product);
|
|
||||||
saleItem.setQuantity(itemRequest.getQuantity());
|
|
||||||
saleItem.setUnitPrice(unitPrice);
|
|
||||||
|
|
||||||
saleItems.add(saleItem);
|
BigDecimal unitPrice = product.getProdPrice();
|
||||||
totalAmount = totalAmount.add(itemTotal);
|
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);
|
sale.setTotalAmount(totalAmount);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ spring:
|
|||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: validate
|
||||||
naming:
|
naming:
|
||||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||||
show-sql: ${JPA_SHOW_SQL:false}
|
show-sql: ${JPA_SHOW_SQL:false}
|
||||||
@@ -29,6 +29,11 @@ spring:
|
|||||||
format_sql: true
|
format_sql: true
|
||||||
dialect: org.hibernate.dialect.MySQLDialect
|
dialect: org.hibernate.dialect.MySQLDialect
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
baseline-on-migrate: true
|
||||||
|
baseline-version: 0
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8080}
|
port: ${SERVER_PORT:8080}
|
||||||
servlet:
|
servlet:
|
||||||
|
|||||||
250
src/main/resources/db/migration/V1__baseline_schema.sql
Normal file
250
src/main/resources/db/migration/V1__baseline_schema.sql
Normal 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;
|
||||||
Reference in New Issue
Block a user