Merge remote-tracking branch 'origin/main' into web-products
This commit is contained in:
@@ -33,6 +33,7 @@ public class DevStackApplication {
|
||||
docker.ensureDockerAvailable();
|
||||
docker.startDatabase();
|
||||
context = new SpringApplicationBuilder(BackendApplication.class)
|
||||
.profiles("local")
|
||||
.initializers(new FlywayContextInitializer())
|
||||
.run(args);
|
||||
context.addApplicationListener(event -> {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.petshop.backend.config;
|
||||
|
||||
import com.petshop.backend.repository.PetRepository;
|
||||
import com.petshop.backend.repository.ProductRepository;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Component
|
||||
@Profile("local")
|
||||
public class LocalCatalogSeedInitializer implements CommandLineRunner {
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final PetRepository petRepository;
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
public LocalCatalogSeedInitializer(DataSource dataSource, PetRepository petRepository, ProductRepository productRepository) {
|
||||
this.dataSource = dataSource;
|
||||
this.petRepository = petRepository;
|
||||
this.productRepository = productRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
if (petRepository.count() > 6 || productRepository.count() > 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8",
|
||||
new ClassPathResource("dev/expand_pet_product_seed.sql"));
|
||||
populator.execute(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,40 @@
|
||||
package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.analytics.DashboardResponse;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.service.AnalyticsService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/analytics")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'STAFF')")
|
||||
public class AnalyticsController {
|
||||
|
||||
private final AnalyticsService analyticsService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AnalyticsController(AnalyticsService analyticsService) {
|
||||
public AnalyticsController(AnalyticsService analyticsService, UserRepository userRepository) {
|
||||
this.analyticsService = analyticsService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public ResponseEntity<DashboardResponse> getDashboard(
|
||||
@RequestParam(defaultValue = "30") int days,
|
||||
@RequestParam(defaultValue = "10") int top) {
|
||||
return ResponseEntity.ok(analyticsService.getDashboardData(days, top));
|
||||
if (days < 1 || days > 365) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "days must be between 1 and 365");
|
||||
}
|
||||
if (top < 1 || top > 50) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "top must be between 1 and 50");
|
||||
}
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ public class CategoryController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<CategoryResponse>> getAllCategories(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String type,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(categoryService.getAllCategories(q, pageable));
|
||||
return ResponseEntity.ok(categoryService.getAllCategories(q, type, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest;
|
||||
import com.petshop.backend.dto.chat.ConversationResponse;
|
||||
import com.petshop.backend.dto.chat.MessageRequest;
|
||||
import com.petshop.backend.dto.chat.MessageResponse;
|
||||
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.CustomerRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
@@ -96,4 +97,13 @@ public class ChatController {
|
||||
chatRealtimeService.publishConversationUpdate(id);
|
||||
return ResponseEntity.ok(conversation);
|
||||
}
|
||||
|
||||
@PutMapping("/conversations/{id}")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<ConversationResponse> updateConversation(@PathVariable Long id, @Valid @RequestBody UpdateConversationRequest request) {
|
||||
User user = getCurrentUser();
|
||||
ConversationResponse conversation = chatService.updateConversation(id, user.getId(), user.getRole(), request);
|
||||
chatRealtimeService.publishConversationUpdate(id);
|
||||
return ResponseEntity.ok(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,29 @@ public class DropdownController {
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/product-categories")
|
||||
public ResponseEntity<List<DropdownOption>> getProductCategories() {
|
||||
return ResponseEntity.ok(
|
||||
categoryRepository.findAll().stream()
|
||||
.filter(c -> "product".equalsIgnoreCase(c.getCategoryType()))
|
||||
.map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName()))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/pet-species")
|
||||
public ResponseEntity<List<DropdownOption>> getPetSpecies() {
|
||||
return ResponseEntity.ok(
|
||||
petRepository.findAll().stream()
|
||||
.map(p -> p.getPetSpecies())
|
||||
.filter(species -> species != null && !species.isBlank())
|
||||
.distinct()
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.map(species -> new DropdownOption(null, species))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/stores")
|
||||
public ResponseEntity<List<DropdownOption>> getStores() {
|
||||
return ResponseEntity.ok(
|
||||
|
||||
@@ -25,8 +25,10 @@ public class PetController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<PetResponse>> getAllPets(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String species,
|
||||
@RequestParam(required = false) String status,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(petService.getAllPets(q, pageable));
|
||||
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -25,8 +25,9 @@ public class ProductController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<ProductResponse>> getAllProducts(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) Long categoryId,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(productService.getAllProducts(q, pageable));
|
||||
return ResponseEntity.ok(productService.getAllProducts(q, categoryId, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -9,15 +9,19 @@ public class DashboardResponse {
|
||||
private InventorySummary inventorySummary;
|
||||
private List<TopProduct> topProducts;
|
||||
private List<DailySales> dailySales;
|
||||
private List<PaymentMethodData> paymentMethods;
|
||||
private List<EmployeePerformanceData> employeePerformance;
|
||||
|
||||
public DashboardResponse() {
|
||||
}
|
||||
|
||||
public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List<TopProduct> topProducts, List<DailySales> dailySales) {
|
||||
public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List<TopProduct> topProducts, List<DailySales> dailySales, List<PaymentMethodData> paymentMethods, List<EmployeePerformanceData> employeePerformance) {
|
||||
this.salesSummary = salesSummary;
|
||||
this.inventorySummary = inventorySummary;
|
||||
this.topProducts = topProducts;
|
||||
this.dailySales = dailySales;
|
||||
this.paymentMethods = paymentMethods;
|
||||
this.employeePerformance = employeePerformance;
|
||||
}
|
||||
|
||||
public SalesSummary getSalesSummary() {
|
||||
@@ -52,17 +56,33 @@ public class DashboardResponse {
|
||||
this.dailySales = dailySales;
|
||||
}
|
||||
|
||||
public List<PaymentMethodData> getPaymentMethods() {
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
public void setPaymentMethods(List<PaymentMethodData> paymentMethods) {
|
||||
this.paymentMethods = paymentMethods;
|
||||
}
|
||||
|
||||
public List<EmployeePerformanceData> getEmployeePerformance() {
|
||||
return employeePerformance;
|
||||
}
|
||||
|
||||
public void setEmployeePerformance(List<EmployeePerformanceData> employeePerformance) {
|
||||
this.employeePerformance = employeePerformance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DashboardResponse that = (DashboardResponse) o;
|
||||
return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales);
|
||||
return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales) && Objects.equals(paymentMethods, that.paymentMethods) && Objects.equals(employeePerformance, that.employeePerformance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales);
|
||||
return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,6 +92,8 @@ public class DashboardResponse {
|
||||
", inventorySummary=" + inventorySummary +
|
||||
", topProducts=" + topProducts +
|
||||
", dailySales=" + dailySales +
|
||||
", paymentMethods=" + paymentMethods +
|
||||
", employeePerformance=" + employeePerformance +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -80,15 +102,17 @@ public class DashboardResponse {
|
||||
private Long totalSales;
|
||||
private BigDecimal totalRefunds;
|
||||
private Long totalRefundCount;
|
||||
private Long totalItemsSold;
|
||||
|
||||
public SalesSummary() {
|
||||
}
|
||||
|
||||
public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount) {
|
||||
public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount, Long totalItemsSold) {
|
||||
this.totalRevenue = totalRevenue;
|
||||
this.totalSales = totalSales;
|
||||
this.totalRefunds = totalRefunds;
|
||||
this.totalRefundCount = totalRefundCount;
|
||||
this.totalItemsSold = totalItemsSold;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalRevenue() {
|
||||
@@ -123,17 +147,25 @@ public class DashboardResponse {
|
||||
this.totalRefundCount = totalRefundCount;
|
||||
}
|
||||
|
||||
public Long getTotalItemsSold() {
|
||||
return totalItemsSold;
|
||||
}
|
||||
|
||||
public void setTotalItemsSold(Long totalItemsSold) {
|
||||
this.totalItemsSold = totalItemsSold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SalesSummary that = (SalesSummary) o;
|
||||
return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount);
|
||||
return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount) && Objects.equals(totalItemsSold, that.totalItemsSold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount);
|
||||
return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,10 +175,69 @@ public class DashboardResponse {
|
||||
", totalSales=" + totalSales +
|
||||
", totalRefunds=" + totalRefunds +
|
||||
", totalRefundCount=" + totalRefundCount +
|
||||
", totalItemsSold=" + totalItemsSold +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentMethodData {
|
||||
private String paymentMethod;
|
||||
private Long count;
|
||||
|
||||
public PaymentMethodData() {
|
||||
}
|
||||
|
||||
public PaymentMethodData(String paymentMethod, Long count) {
|
||||
this.paymentMethod = paymentMethod;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getPaymentMethod() {
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
public void setPaymentMethod(String paymentMethod) {
|
||||
this.paymentMethod = paymentMethod;
|
||||
}
|
||||
|
||||
public Long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(Long count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EmployeePerformanceData {
|
||||
private String employeeName;
|
||||
private BigDecimal revenue;
|
||||
|
||||
public EmployeePerformanceData() {
|
||||
}
|
||||
|
||||
public EmployeePerformanceData(String employeeName, BigDecimal revenue) {
|
||||
this.employeeName = employeeName;
|
||||
this.revenue = revenue;
|
||||
}
|
||||
|
||||
public String getEmployeeName() {
|
||||
return employeeName;
|
||||
}
|
||||
|
||||
public void setEmployeeName(String employeeName) {
|
||||
this.employeeName = employeeName;
|
||||
}
|
||||
|
||||
public BigDecimal getRevenue() {
|
||||
return revenue;
|
||||
}
|
||||
|
||||
public void setRevenue(BigDecimal revenue) {
|
||||
this.revenue = revenue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class InventorySummary {
|
||||
private Long totalProducts;
|
||||
private Long lowStockProducts;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.petshop.backend.dto.chat;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
public class UpdateConversationRequest {
|
||||
@NotBlank(message = "Status is required")
|
||||
@Pattern(regexp = "^(OPEN|CLOSED)$", message = "Status must be OPEN or CLOSED")
|
||||
private String status;
|
||||
|
||||
public UpdateConversationRequest() {
|
||||
}
|
||||
|
||||
public UpdateConversationRequest(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||
Optional<Category> findByCategoryName(String categoryName);
|
||||
|
||||
@Query("SELECT c FROM Category c WHERE " +
|
||||
"LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Category> searchCategories(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:type IS NULL OR LOWER(c.categoryType) = LOWER(:type))")
|
||||
Page<Category> searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository;
|
||||
public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
|
||||
@Query("SELECT p FROM Pet p WHERE " +
|
||||
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Pet> searchPets(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
|
||||
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository;
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
@Query("SELECT p FROM Product p WHERE " +
|
||||
"LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Product> searchProducts(@Param("q") String query, Pageable pageable);
|
||||
"(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
|
||||
Page<Product> searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.dto.analytics.DashboardResponse;
|
||||
import com.petshop.backend.entity.Employee;
|
||||
import com.petshop.backend.entity.Inventory;
|
||||
import com.petshop.backend.entity.Product;
|
||||
import com.petshop.backend.entity.Sale;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.EmployeeRepository;
|
||||
import com.petshop.backend.repository.InventoryRepository;
|
||||
import com.petshop.backend.repository.ProductRepository;
|
||||
import com.petshop.backend.repository.SaleRepository;
|
||||
@@ -23,28 +26,33 @@ public class AnalyticsService {
|
||||
private final SaleRepository saleRepository;
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final EmployeeRepository employeeRepository;
|
||||
|
||||
public AnalyticsService(SaleRepository saleRepository,
|
||||
InventoryRepository inventoryRepository, ProductRepository productRepository) {
|
||||
InventoryRepository inventoryRepository, ProductRepository productRepository, EmployeeRepository employeeRepository) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.employeeRepository = employeeRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DashboardResponse getDashboardData(int days, int top) {
|
||||
public DashboardResponse getDashboardData(int days, int top, User user) {
|
||||
LocalDateTime startDate = LocalDateTime.now().minusDays(days);
|
||||
|
||||
List<Sale> sales = saleRepository.findAll().stream()
|
||||
.filter(sale -> sale.getSaleDate().isAfter(startDate))
|
||||
.filter(sale -> includeSaleForUser(sale, user))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales);
|
||||
DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary();
|
||||
DashboardResponse.InventorySummary inventorySummary = user.getRole() == User.Role.ADMIN ? calculateInventorySummary() : null;
|
||||
List<DashboardResponse.TopProduct> topProducts = calculateTopProducts(sales, top);
|
||||
List<DashboardResponse.DailySales> dailySales = calculateDailySales(sales, days);
|
||||
List<DashboardResponse.PaymentMethodData> paymentMethods = calculatePaymentMethods(sales);
|
||||
List<DashboardResponse.EmployeePerformanceData> employeePerformance = calculateEmployeePerformance(sales, user);
|
||||
|
||||
return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales);
|
||||
return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance);
|
||||
}
|
||||
|
||||
private DashboardResponse.SalesSummary calculateSalesSummary(List<Sale> sales) {
|
||||
@@ -66,7 +74,13 @@ public class AnalyticsService {
|
||||
.filter(Sale::getIsRefund)
|
||||
.count();
|
||||
|
||||
return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount);
|
||||
Long totalItemsSold = sales.stream()
|
||||
.filter(sale -> !sale.getIsRefund())
|
||||
.flatMap(sale -> sale.getItems().stream())
|
||||
.mapToLong(item -> item.getQuantity())
|
||||
.sum();
|
||||
|
||||
return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold);
|
||||
}
|
||||
|
||||
private DashboardResponse.InventorySummary calculateInventorySummary() {
|
||||
@@ -93,6 +107,9 @@ public class AnalyticsService {
|
||||
Map<Long, DashboardResponse.TopProduct> productSalesMap = new HashMap<>();
|
||||
|
||||
for (Sale sale : sales) {
|
||||
if (sale.getIsRefund()) {
|
||||
continue;
|
||||
}
|
||||
for (var item : sale.getItems()) {
|
||||
Long productId = item.getProduct().getProdId();
|
||||
String productName = item.getProduct().getProdName();
|
||||
@@ -128,6 +145,9 @@ public class AnalyticsService {
|
||||
}
|
||||
|
||||
for (Sale sale : sales) {
|
||||
if (sale.getIsRefund()) {
|
||||
continue;
|
||||
}
|
||||
LocalDate saleDate = sale.getSaleDate().toLocalDate();
|
||||
if (dailySalesMap.containsKey(saleDate)) {
|
||||
DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate);
|
||||
@@ -138,4 +158,50 @@ public class AnalyticsService {
|
||||
|
||||
return new ArrayList<>(dailySalesMap.values());
|
||||
}
|
||||
|
||||
private List<DashboardResponse.PaymentMethodData> calculatePaymentMethods(List<Sale> sales) {
|
||||
return sales.stream()
|
||||
.filter(sale -> !sale.getIsRefund())
|
||||
.collect(Collectors.groupingBy(
|
||||
sale -> sale.getPaymentMethod() == null ? "Unknown" : sale.getPaymentMethod(),
|
||||
TreeMap::new,
|
||||
Collectors.counting()))
|
||||
.entrySet().stream()
|
||||
.map(entry -> new DashboardResponse.PaymentMethodData(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<DashboardResponse.EmployeePerformanceData> calculateEmployeePerformance(List<Sale> sales, User user) {
|
||||
Map<String, BigDecimal> employeeRevenue = new TreeMap<>();
|
||||
|
||||
for (Sale sale : sales) {
|
||||
if (sale.getIsRefund()) {
|
||||
continue;
|
||||
}
|
||||
String employeeName = sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName();
|
||||
employeeRevenue.merge(employeeName, sale.getTotalAmount(), BigDecimal::add);
|
||||
}
|
||||
|
||||
if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) {
|
||||
Employee employee = employeeRepository.findByUserId(user.getId()).orElse(null);
|
||||
if (employee != null) {
|
||||
String employeeName = employee.getFirstName() + " " + employee.getLastName();
|
||||
employeeRevenue.put(employeeName, BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
return employeeRevenue.entrySet().stream()
|
||||
.map(entry -> new DashboardResponse.EmployeePerformanceData(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean includeSaleForUser(Sale sale, User user) {
|
||||
if (user.getRole() == User.Role.ADMIN) {
|
||||
return true;
|
||||
}
|
||||
if (user.getRole() == User.Role.STAFF) {
|
||||
return sale.getEmployee() != null && sale.getEmployee().getUserId() != null && sale.getEmployee().getUserId().equals(user.getId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ public class AppointmentService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
public List<String> checkAvailability(Long storeId, Long serviceId, LocalDate date) {
|
||||
storeRepository.findById(storeId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
|
||||
@@ -200,7 +201,16 @@ public class AppointmentService {
|
||||
com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId));
|
||||
|
||||
List<Appointment> existingAppointments = appointmentRepository.findByStoreAndDate(storeId, date);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// CHANGED: filter by serviceId too
|
||||
List<Appointment> existingAppointments = appointmentRepository
|
||||
.findByStoreAndDate(storeId, date)
|
||||
.stream()
|
||||
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||
.collect(Collectors.toList());
|
||||
// -------------------------------------------------------
|
||||
|
||||
|
||||
List<String> availableSlots = new ArrayList<>();
|
||||
LocalTime startTime = LocalTime.of(9, 0);
|
||||
@@ -286,13 +296,21 @@ public class AppointmentService {
|
||||
return response;
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
|
||||
List<Appointment> existingAppointments = appointmentRepository.findByStoreAndDate(store.getStoreId(), date);
|
||||
// Filter by same service only - different services can run at same time
|
||||
List<Appointment> existingAppointments = appointmentRepository
|
||||
.findByStoreAndDate(store.getStoreId(), date)
|
||||
.stream()
|
||||
.filter(a -> a.getService().getServiceId().equals(service.getServiceId()))
|
||||
.collect(Collectors.toList());
|
||||
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
|
||||
throw new IllegalArgumentException("Appointment time is not available for the selected store and service");
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
|
||||
private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) {
|
||||
LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration());
|
||||
for (Appointment existingAppointment : existingAppointments) {
|
||||
|
||||
@@ -20,14 +20,9 @@ public class CategoryService {
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
public Page<CategoryResponse> getAllCategories(String query, Pageable pageable) {
|
||||
Page<Category> categories;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
categories = categoryRepository.searchCategories(query, pageable);
|
||||
} else {
|
||||
categories = categoryRepository.findAll(pageable);
|
||||
}
|
||||
return categories.map(this::mapToResponse);
|
||||
public Page<CategoryResponse> getAllCategories(String query, String type, Pageable pageable) {
|
||||
return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public CategoryResponse getCategoryById(Long id) {
|
||||
@@ -80,4 +75,12 @@ public class CategoryService {
|
||||
category.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest;
|
||||
import com.petshop.backend.dto.chat.ConversationResponse;
|
||||
import com.petshop.backend.dto.chat.MessageRequest;
|
||||
import com.petshop.backend.dto.chat.MessageResponse;
|
||||
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||
import com.petshop.backend.entity.Conversation;
|
||||
import com.petshop.backend.entity.Customer;
|
||||
import com.petshop.backend.entity.Message;
|
||||
@@ -116,6 +117,10 @@ public class ChatService {
|
||||
Conversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||
|
||||
if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) {
|
||||
throw new AccessDeniedException("Conversation is closed");
|
||||
}
|
||||
|
||||
if (!hasConversationAccess(conversation, userId, role)) {
|
||||
if (role == User.Role.CUSTOMER) {
|
||||
throw new AccessDeniedException("You can only send messages to your own conversations");
|
||||
@@ -149,6 +154,10 @@ public class ChatService {
|
||||
Conversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||
|
||||
if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) {
|
||||
throw new AccessDeniedException("Conversation is closed");
|
||||
}
|
||||
|
||||
if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) {
|
||||
throw new AccessDeniedException("You can only request human takeover for your own conversations");
|
||||
}
|
||||
@@ -163,6 +172,28 @@ public class ChatService {
|
||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ConversationResponse updateConversation(Long conversationId, Long userId, User.Role role, UpdateConversationRequest request) {
|
||||
Conversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||
|
||||
if (!hasConversationAccess(conversation, userId, role)) {
|
||||
if (role == User.Role.CUSTOMER) {
|
||||
throw new AccessDeniedException("You can only close your own conversations");
|
||||
}
|
||||
if (role == User.Role.STAFF) {
|
||||
throw new AccessDeniedException("You can only close conversations assigned to you or unassigned conversations");
|
||||
}
|
||||
}
|
||||
|
||||
conversation.setStatus(Conversation.ConversationStatus.valueOf(request.getStatus()));
|
||||
conversation = conversationRepository.save(conversation);
|
||||
|
||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||
String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent();
|
||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
||||
}
|
||||
|
||||
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
||||
Conversation conversation = conversationRepository.findById(conversationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||
|
||||
@@ -4,23 +4,34 @@ import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.dto.customer.CustomerRequest;
|
||||
import com.petshop.backend.dto.customer.CustomerResponse;
|
||||
import com.petshop.backend.entity.Customer;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||
import com.petshop.backend.repository.CustomerRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||
|
||||
@Service
|
||||
public class CustomerService {
|
||||
|
||||
private static final String TEMP_PASSWORD = "TempPass123!";
|
||||
|
||||
private final CustomerRepository customerRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserBusinessLinkageService userBusinessLinkageService;
|
||||
|
||||
public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) {
|
||||
public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
|
||||
this.customerRepository = customerRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userBusinessLinkageService = userBusinessLinkageService;
|
||||
}
|
||||
|
||||
public Page<CustomerResponse> getAllCustomers(String query, Pageable pageable) {
|
||||
@@ -41,14 +52,19 @@ public class CustomerService {
|
||||
|
||||
@Transactional
|
||||
public CustomerResponse createCustomer(CustomerRequest request) {
|
||||
ensureEmailAvailable(request.getEmail(), null);
|
||||
|
||||
Customer customer = new Customer();
|
||||
customer.setFirstName(request.getFirstName());
|
||||
customer.setLastName(request.getLastName());
|
||||
customer.setEmail(request.getEmail());
|
||||
|
||||
customer = customerRepository.save(customer);
|
||||
syncLinkedUser(customer);
|
||||
return mapToResponse(customer);
|
||||
User user = createLinkedUser(customer);
|
||||
|
||||
Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user);
|
||||
syncLinkedUser(linkedCustomer);
|
||||
return mapToResponse(linkedCustomer);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -56,6 +72,8 @@ public class CustomerService {
|
||||
Customer customer = customerRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id));
|
||||
|
||||
ensureEmailAvailable(request.getEmail(), customer.getUserId());
|
||||
|
||||
customer.setFirstName(request.getFirstName());
|
||||
customer.setLastName(request.getLastName());
|
||||
customer.setEmail(request.getEmail());
|
||||
@@ -67,9 +85,14 @@ public class CustomerService {
|
||||
|
||||
@Transactional
|
||||
public void deleteCustomer(Long id) {
|
||||
if (!customerRepository.existsById(id)) {
|
||||
throw new ResourceNotFoundException("Customer not found with id: " + id);
|
||||
Customer customer = customerRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id));
|
||||
|
||||
if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) {
|
||||
userRepository.deleteById(customer.getUserId());
|
||||
return;
|
||||
}
|
||||
|
||||
customerRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -99,4 +122,37 @@ public class CustomerService {
|
||||
userRepository.save(user);
|
||||
});
|
||||
}
|
||||
|
||||
private User createLinkedUser(Customer customer) {
|
||||
User user = new User();
|
||||
user.setUsername(generateUsername(customer));
|
||||
user.setPassword(passwordEncoder.encode(TEMP_PASSWORD));
|
||||
user.setEmail(customer.getEmail());
|
||||
user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim());
|
||||
user.setPhone(generatePhone(customer));
|
||||
user.setRole(User.Role.CUSTOMER);
|
||||
user.setActive(false);
|
||||
user.setTokenVersion(0);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
private String generateUsername(Customer customer) {
|
||||
return "customer_" + customer.getCustomerId();
|
||||
}
|
||||
|
||||
private String generatePhone(Customer customer) {
|
||||
return String.format("200-000-%04d", customer.getCustomerId());
|
||||
}
|
||||
|
||||
private void ensureEmailAvailable(String email, Long currentUserId) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
userRepository.findByEmail(email).ifPresent(existing -> {
|
||||
if (currentUserId == null || !existing.getId().equals(currentUserId)) {
|
||||
throw new ResponseStatusException(CONFLICT, "Email already exists");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,9 @@ public class PetService {
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
}
|
||||
|
||||
public Page<PetResponse> getAllPets(String query, Pageable pageable) {
|
||||
Page<Pet> pets;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
pets = petRepository.searchPets(query, pageable);
|
||||
} else {
|
||||
pets = petRepository.findAll(pageable);
|
||||
}
|
||||
return pets.map(this::mapToResponse);
|
||||
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
|
||||
return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public PetResponse getPetById(Long id) {
|
||||
@@ -182,6 +177,14 @@ public class PetService {
|
||||
return status == null ? "" : status.trim();
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private PetResponse mapToResponse(Pet pet) {
|
||||
return new PetResponse(
|
||||
pet.getPetId(),
|
||||
|
||||
@@ -32,14 +32,9 @@ public class ProductService {
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
}
|
||||
|
||||
public Page<ProductResponse> getAllProducts(String query, Pageable pageable) {
|
||||
Page<Product> products;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
products = productRepository.searchProducts(query, pageable);
|
||||
} else {
|
||||
products = productRepository.findAll(pageable);
|
||||
}
|
||||
return products.map(this::mapToResponse);
|
||||
public Page<ProductResponse> getAllProducts(String query, Long categoryId, Pageable pageable) {
|
||||
return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable)
|
||||
.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
public ProductResponse getProductById(Long id) {
|
||||
@@ -168,4 +163,12 @@ public class ProductService {
|
||||
|
||||
public record ImagePayload(Resource resource, MediaType mediaType) {
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SaleService {
|
||||
sale.setSaleDate(LocalDateTime.now());
|
||||
sale.setEmployee(employee);
|
||||
sale.setStore(store);
|
||||
sale.setPaymentMethod(request.getPaymentMethod());
|
||||
sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod()));
|
||||
sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false);
|
||||
|
||||
if (request.getCustomerId() != null) {
|
||||
@@ -215,4 +215,22 @@ public class SaleService {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
String normalizePaymentMethod(String paymentMethod) {
|
||||
if (paymentMethod == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = paymentMethod.trim();
|
||||
if (normalized.equalsIgnoreCase("Debit")) {
|
||||
return "Card";
|
||||
}
|
||||
if (normalized.equalsIgnoreCase("Cash")) {
|
||||
return "Cash";
|
||||
}
|
||||
if (normalized.equalsIgnoreCase("Card")) {
|
||||
return "Card";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ spring:
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
address: ${SERVER_ADDRESS:0.0.0.0}
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE sale
|
||||
SET paymentMethod = 'Card'
|
||||
WHERE LOWER(paymentMethod) = 'debit';
|
||||
@@ -128,7 +128,7 @@ VALUES
|
||||
('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1),
|
||||
('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2),
|
||||
('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3),
|
||||
('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1),
|
||||
('2026-01-15 10:45:00', 150.00, 'Card', 1, 1, 1),
|
||||
('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2),
|
||||
('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL),
|
||||
('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4),
|
||||
@@ -136,12 +136,12 @@ VALUES
|
||||
('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1),
|
||||
('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3),
|
||||
('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL),
|
||||
('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2),
|
||||
('2026-02-08 16:15:00', 160.00, 'Card', 1, 1, 2),
|
||||
('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL),
|
||||
('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1),
|
||||
('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL),
|
||||
('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4),
|
||||
('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL),
|
||||
('2026-02-20 14:35:00', 155.00, 'Card', 4, 3, NULL),
|
||||
('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2),
|
||||
('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL),
|
||||
(NOW(), 95.00, 'Card', 1, 1, 1);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion)
|
||||
SELECT
|
||||
CONCAT('customer_', c.customerId) AS username,
|
||||
'$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password,
|
||||
CASE
|
||||
WHEN c.email IS NOT NULL
|
||||
AND c.email <> ''
|
||||
AND (SELECT COUNT(*) FROM customer c2 WHERE c2.email = c.email) = 1
|
||||
AND NOT EXISTS (SELECT 1 FROM employee e2 WHERE e2.email = c.email)
|
||||
AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = c.email)
|
||||
THEN c.email
|
||||
ELSE CONCAT('customer_', c.customerId, '@petshop.local')
|
||||
END AS email,
|
||||
CONCAT(c.firstName, ' ', c.lastName) AS fullName,
|
||||
CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone,
|
||||
'CUSTOMER' AS role,
|
||||
FALSE AS active,
|
||||
0 AS tokenVersion
|
||||
FROM customer c
|
||||
WHERE c.user_id IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users u
|
||||
WHERE u.username = CONCAT('customer_', c.customerId)
|
||||
);
|
||||
|
||||
INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion)
|
||||
SELECT
|
||||
CONCAT('employee_', e.employeeId) AS username,
|
||||
'$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password,
|
||||
CASE
|
||||
WHEN e.email IS NOT NULL
|
||||
AND e.email <> ''
|
||||
AND (SELECT COUNT(*) FROM employee e2 WHERE e2.email = e.email) = 1
|
||||
AND NOT EXISTS (SELECT 1 FROM customer c2 WHERE c2.email = e.email)
|
||||
AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = e.email)
|
||||
THEN e.email
|
||||
ELSE CONCAT('employee_', e.employeeId, '@petshop.local')
|
||||
END AS email,
|
||||
CONCAT(e.firstName, ' ', e.lastName) AS fullName,
|
||||
CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone,
|
||||
CASE
|
||||
WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN'
|
||||
ELSE 'STAFF'
|
||||
END AS role,
|
||||
FALSE AS active,
|
||||
0 AS tokenVersion
|
||||
FROM employee e
|
||||
WHERE e.user_id IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users u
|
||||
WHERE u.username = CONCAT('employee_', e.employeeId)
|
||||
);
|
||||
|
||||
UPDATE customer c
|
||||
JOIN users u ON u.username = CONCAT('customer_', c.customerId)
|
||||
AND u.role = 'CUSTOMER'
|
||||
SET c.user_id = u.id
|
||||
WHERE c.user_id IS NULL;
|
||||
|
||||
UPDATE employee e
|
||||
JOIN users u ON u.username = CONCAT('employee_', e.employeeId)
|
||||
AND u.role IN ('STAFF', 'ADMIN')
|
||||
SET e.user_id = u.id
|
||||
WHERE e.user_id IS NULL;
|
||||
|
||||
UPDATE users
|
||||
SET
|
||||
fullName = CASE
|
||||
WHEN fullName IS NULL OR fullName = '' THEN username
|
||||
ELSE fullName
|
||||
END,
|
||||
email = CASE
|
||||
WHEN email IS NULL OR email = '' THEN CONCAT(username, '@petshop.local')
|
||||
ELSE email
|
||||
END,
|
||||
phone = CASE
|
||||
WHEN phone IS NULL OR phone = '' THEN CONCAT('000-000-', LPAD(id, 4, '0'))
|
||||
ELSE phone
|
||||
END,
|
||||
active = COALESCE(active, TRUE),
|
||||
tokenVersion = COALESCE(tokenVersion, 0)
|
||||
WHERE fullName IS NULL
|
||||
OR fullName = ''
|
||||
OR email IS NULL
|
||||
OR email = ''
|
||||
OR phone IS NULL
|
||||
OR phone = ''
|
||||
OR active IS NULL
|
||||
OR tokenVersion IS NULL;
|
||||
226
backend/src/main/resources/dev/expand_pet_product_seed.sql
Normal file
226
backend/src/main/resources/dev/expand_pet_product_seed.sql
Normal file
@@ -0,0 +1,226 @@
|
||||
-- Expand pet and product seed data
|
||||
|
||||
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice)
|
||||
VALUES
|
||||
('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00),
|
||||
('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00),
|
||||
('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00),
|
||||
('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00),
|
||||
('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00),
|
||||
('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00),
|
||||
('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00),
|
||||
('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00),
|
||||
('Leo', 'Dog', 'Husky', 4, 'Available', 749.00),
|
||||
('Penny', 'Dog', 'Border Collie', 5, 'Available', 808.00),
|
||||
('Jax', 'Dog', 'German Shepherd', 1, 'Available', 823.00),
|
||||
('Nala', 'Dog', 'Poodle', 2, 'Available', 871.00),
|
||||
('Finn', 'Dog', 'Bulldog', 3, 'Available', 447.00),
|
||||
('Sadie', 'Dog', 'Boxer', 4, 'Available', 495.00),
|
||||
('Ace', 'Dog', 'Dachshund', 5, 'Available', 510.00),
|
||||
('Zoe', 'Dog', 'Shih Tzu', 1, 'Available', 547.00),
|
||||
('Ollie', 'Dog', 'Rottweiler', 2, 'Available', 606.00),
|
||||
('Millie', 'Dog', 'Corgi', 3, 'Available', 654.00),
|
||||
('Murphy', 'Dog', 'Husky', 4, 'Available', 691.00),
|
||||
('Willow', 'Dog', 'Border Collie', 5, 'Available', 728.00),
|
||||
('Bentley', 'Dog', 'German Shepherd', 1, 'Available', 776.00),
|
||||
('Lily', 'Dog', 'Poodle', 2, 'Available', 780.00),
|
||||
('Scout', 'Dog', 'Bulldog', 3, 'Available', 828.00),
|
||||
('Gracie', 'Dog', 'Boxer', 4, 'Available', 876.00),
|
||||
('Ranger', 'Dog', 'Dachshund', 5, 'Available', 452.00),
|
||||
('Hazel', 'Dog', 'Shih Tzu', 1, 'Available', 478.00),
|
||||
('Moose', 'Dog', 'Rottweiler', 2, 'Available', 515.00),
|
||||
('Mia', 'Dog', 'Corgi', 3, 'Available', 530.00),
|
||||
('Simba', 'Cat', 'Ragdoll', 1, 'Available', 295.00),
|
||||
('Cleo', 'Cat', 'Bengal', 2, 'Available', 321.00),
|
||||
('Oreo', 'Cat', 'British Shorthair', 3, 'Available', 358.00),
|
||||
('Pepper', 'Cat', 'Sphynx', 4, 'Available', 417.00),
|
||||
('Jasper', 'Cat', 'Scottish Fold', 5, 'Available', 454.00),
|
||||
('Phoebe', 'Cat', 'Russian Blue', 1, 'Available', 491.00),
|
||||
('Shadow', 'Cat', 'Abyssinian', 2, 'Available', 528.00),
|
||||
('Mochi', 'Cat', 'Birman', 3, 'Available', 554.00),
|
||||
('Louie', 'Cat', 'Ragdoll', 4, 'Available', 591.00),
|
||||
('Ivy', 'Cat', 'Bengal', 5, 'Available', 606.00),
|
||||
('Theo', 'Cat', 'British Shorthair', 1, 'Available', 654.00),
|
||||
('Piper', 'Cat', 'Sphynx', 2, 'Available', 251.00),
|
||||
('Nova', 'Cat', 'Scottish Fold', 3, 'Available', 277.00),
|
||||
('Archie', 'Cat', 'Russian Blue', 4, 'Available', 336.00),
|
||||
('Olive', 'Cat', 'Abyssinian', 5, 'Available', 362.00),
|
||||
('Boots', 'Cat', 'Birman', 1, 'Available', 399.00),
|
||||
('Maple', 'Cat', 'Ragdoll', 2, 'Available', 436.00),
|
||||
('Gizmo', 'Cat', 'Bengal', 3, 'Available', 473.00),
|
||||
('Nina', 'Cat', 'British Shorthair', 4, 'Available', 499.00),
|
||||
('Salem', 'Cat', 'Sphynx', 5, 'Available', 547.00),
|
||||
('Stella', 'Cat', 'Scottish Fold', 1, 'Available', 595.00),
|
||||
('Kiki', 'Cat', 'Russian Blue', 2, 'Available', 610.00),
|
||||
('Sunny', 'Cat', 'Abyssinian', 3, 'Available', 658.00),
|
||||
('Mabel', 'Cat', 'Birman', 4, 'Available', 244.00),
|
||||
('Coco', 'Bird', 'Cockatiel', 1, 'Available', 119.00),
|
||||
('Sky', 'Bird', 'Parakeet', 2, 'Available', 145.00),
|
||||
('Sunny', 'Bird', 'Canary', 3, 'Available', 204.00),
|
||||
('Kiwi', 'Bird', 'Lovebird', 1, 'Available', 230.00),
|
||||
('Pico', 'Bird', 'Finch', 2, 'Available', 81.00),
|
||||
('Blue', 'Bird', 'Conure', 3, 'Available', 118.00),
|
||||
('Rio', 'Bird', 'Cockatiel', 1, 'Available', 144.00),
|
||||
('Angel', 'Bird', 'Parakeet', 2, 'Available', 203.00),
|
||||
('Chirpy', 'Bird', 'Canary', 3, 'Available', 251.00),
|
||||
('Peach', 'Bird', 'Lovebird', 1, 'Available', 91.00),
|
||||
('Mango', 'Bird', 'Finch', 2, 'Available', 128.00),
|
||||
('Pearl', 'Bird', 'Conure', 3, 'Available', 165.00),
|
||||
('Bubbles', 'Fish', 'Goldfish', 1, 'Available', 30.00),
|
||||
('Splash', 'Fish', 'Betta', 2, 'Available', 56.00),
|
||||
('Coral', 'Fish', 'Guppy', 1, 'Available', 23.00),
|
||||
('Neptune', 'Fish', 'Molly', 2, 'Available', 23.00),
|
||||
('Marlin', 'Fish', 'Tetra', 1, 'Available', 49.00),
|
||||
('Finley', 'Fish', 'Angelfish', 2, 'Available', 27.00),
|
||||
('Pebble', 'Fish', 'Goldfish', 1, 'Available', 64.00),
|
||||
('Wave', 'Fish', 'Betta', 2, 'Available', 20.00),
|
||||
('Aqua', 'Fish', 'Guppy', 1, 'Available', 57.00),
|
||||
('Flash', 'Fish', 'Molly', 2, 'Available', 46.00),
|
||||
('Nemo', 'Fish', 'Tetra', 1, 'Available', 13.00),
|
||||
('Pearl', 'Fish', 'Angelfish', 2, 'Available', 61.00),
|
||||
('Thumper', 'Rabbit', 'Mini Lop', 1, 'Adopted', 147.00),
|
||||
('Clover', 'Rabbit', 'Netherland Dwarf', 2, 'Adopted', 138.00),
|
||||
('Biscuit', 'Rabbit', 'Lionhead', 3, 'Adopted', 177.00),
|
||||
('Hazel', 'Rabbit', 'Rex', 1, 'Adopted', 91.00),
|
||||
('Juniper', 'Rabbit', 'Mini Lop', 2, 'Adopted', 83.00),
|
||||
('Poppy', 'Rabbit', 'Netherland Dwarf', 3, 'Adopted', 111.00),
|
||||
('Snowball', 'Rabbit', 'Lionhead', 1, 'Adopted', 172.00),
|
||||
('Maple', 'Rabbit', 'Rex', 2, 'Adopted', 150.00),
|
||||
('Peanut', 'Hamster', 'Syrian', 1, 'Adopted', 29.00),
|
||||
('Nibbles', 'Hamster', 'Dwarf', 2, 'Adopted', 42.00),
|
||||
('Pumpkin', 'Hamster', 'Roborovski', 1, 'Pending', 49.00),
|
||||
('Mocha', 'Hamster', 'Syrian', 2, 'Pending', 48.00),
|
||||
('Buttons', 'Hamster', 'Dwarf', 1, 'Pending', 61.00),
|
||||
('Teddy', 'Hamster', 'Roborovski', 2, 'Pending', 35.00),
|
||||
('Pip', 'Hamster', 'Syrian', 1, 'Pending', 39.00),
|
||||
('Toffee', 'Hamster', 'Dwarf', 2, 'Pending', 52.00),
|
||||
('Sprout', 'Hamster', 'Roborovski', 1, 'Available', 26.00),
|
||||
('Bean', 'Hamster', 'Syrian', 2, 'Available', 28.00);
|
||||
|
||||
INSERT INTO product (prodName, prodPrice, categoryId, prodDesc)
|
||||
VALUES
|
||||
('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Beef Feast Dog Food', 51.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Salmon Blend Dog Food', 17.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Lamb Dinner Dog Food', 28.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Puppy Starter Kibble', 39.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Senior Care Dog Food', 40.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Small Breed Kibble', 44.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Large Breed Kibble', 57.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Grain Free Dog Food', 68.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Turkey Rice Formula', 79.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Duck Sweet Potato Meal', 25.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Venison Protein Blend', 36.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Healthy Weight Dog Food', 48.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Sensitive Stomach Kibble', 62.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('High Energy Dog Food', 72.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Organic Dog Biscuits', 18.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Peanut Butter Dog Treats', 33.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Dental Chew Sticks', 38.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Training Treat Bites', 48.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Soft Chicken Treats', 57.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Pumpkin Fiber Treats', 70.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Joint Support Biscuits', 14.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Mini Breed Dinner', 17.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Farmhouse Dog Meal', 30.00, 1, 'Nutritious food and treats for dogs'),
|
||||
('Feather Teaser Wand', 30.00, 2, 'Play items for active cats'),
|
||||
('Catnip Mouse Toy', 24.00, 2, 'Play items for active cats'),
|
||||
('Jingle Ball Set', 18.00, 2, 'Play items for active cats'),
|
||||
('Scratching Post Small', 6.00, 2, 'Play items for active cats'),
|
||||
('Crinkle Tunnel', 31.00, 2, 'Play items for active cats'),
|
||||
('Laser Pointer Toy', 6.00, 2, 'Play items for active cats'),
|
||||
('Plush Fish Toy', 19.00, 2, 'Play items for active cats'),
|
||||
('Spring Coil Pack', 20.00, 2, 'Play items for active cats'),
|
||||
('Hanging Door Toy', 12.00, 2, 'Play items for active cats'),
|
||||
('Interactive Puzzle Toy', 22.00, 2, 'Play items for active cats'),
|
||||
('Catnip Kicker Toy', 20.00, 2, 'Play items for active cats'),
|
||||
('Rolling Bell Ball', 20.00, 2, 'Play items for active cats'),
|
||||
('Ribbon Chase Toy', 19.00, 2, 'Play items for active cats'),
|
||||
('Mini Plush Mouse', 21.00, 2, 'Play items for active cats'),
|
||||
('Treat Dispensing Ball', 16.00, 2, 'Play items for active cats'),
|
||||
('Double Pom Toy', 12.00, 2, 'Play items for active cats'),
|
||||
('Window Perch Toy', 10.00, 2, 'Play items for active cats'),
|
||||
('Scratch Pad Refill', 8.00, 2, 'Play items for active cats'),
|
||||
('Rainbow Wand Toy', 23.00, 2, 'Play items for active cats'),
|
||||
('Carpet Scratcher', 23.00, 2, 'Play items for active cats'),
|
||||
('Bird Perch Set', 27.00, 3, 'Care supplies for pet birds'),
|
||||
('Parakeet Seed Mix', 40.00, 3, 'Care supplies for pet birds'),
|
||||
('Canary Food Blend', 53.00, 3, 'Care supplies for pet birds'),
|
||||
('Mineral Cuttlebone', 57.00, 3, 'Care supplies for pet birds'),
|
||||
('Bird Ladder Toy', 68.00, 3, 'Care supplies for pet birds'),
|
||||
('Mirror Bell Combo', 80.00, 3, 'Care supplies for pet birds'),
|
||||
('Clip On Food Cup', 92.00, 3, 'Care supplies for pet birds'),
|
||||
('Bird Cage Liner Pack', 108.00, 3, 'Care supplies for pet birds'),
|
||||
('Nesting Material Pack', 121.00, 3, 'Care supplies for pet birds'),
|
||||
('Treat Spray Millet', 8.00, 3, 'Care supplies for pet birds'),
|
||||
('Wooden Swing Perch', 22.00, 3, 'Care supplies for pet birds'),
|
||||
('Foraging Ball Toy', 32.00, 3, 'Care supplies for pet birds'),
|
||||
('Cage Cleaning Spray', 47.00, 3, 'Care supplies for pet birds'),
|
||||
('Parrot Rope Perch', 54.00, 3, 'Care supplies for pet birds'),
|
||||
('Bird Bath Dish', 54.00, 3, 'Care supplies for pet birds'),
|
||||
('Songbird Vitamin Drops', 78.00, 3, 'Care supplies for pet birds'),
|
||||
('Aquarium Filter Cartridge', 36.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Decorative Aquarium Gravel', 49.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Fish Net Medium', 34.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Water Conditioner', 45.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Aquarium Thermometer', 59.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('LED Tank Light', 67.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Air Stone Pack', 76.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Aquarium Heater 50W', 92.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Aquarium Heater 100W', 106.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Fish Flake Food', 95.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Algae Scraper', 105.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Aquarium Plant Set', 122.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Bubble Curtain Kit', 136.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Breeder Box Insert', 149.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Filter Sponge Pack', 164.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Aquarium Background Roll', 183.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Glass Lid Clips', 174.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Submersible Pump', 191.00, 4, 'Essential aquarium equipment and accessories'),
|
||||
('Hamster Bedding Pack', 50.00, 5, 'Supplies for small pets'),
|
||||
('Rabbit Hay Bundle', 49.00, 5, 'Supplies for small pets'),
|
||||
('Guinea Pig Pellets', 15.00, 5, 'Supplies for small pets'),
|
||||
('Small Pet Water Bottle', 31.00, 5, 'Supplies for small pets'),
|
||||
('Hamster Hideout Hut', 40.00, 5, 'Supplies for small pets'),
|
||||
('Chew Stick Bundle', 48.00, 5, 'Supplies for small pets'),
|
||||
('Rabbit Litter Tray', 58.00, 5, 'Supplies for small pets'),
|
||||
('Exercise Ball Large', 68.00, 5, 'Supplies for small pets'),
|
||||
('Small Pet Food Bowl', 20.00, 5, 'Supplies for small pets'),
|
||||
('Timothy Hay Cubes', 28.00, 5, 'Supplies for small pets'),
|
||||
('Guinea Pig Tunnel', 38.00, 5, 'Supplies for small pets'),
|
||||
('Hamster Nesting Fluff', 47.00, 5, 'Supplies for small pets'),
|
||||
('Rabbit Grooming Brush', 60.00, 5, 'Supplies for small pets'),
|
||||
('Small Pet Carrier', 7.00, 5, 'Supplies for small pets'),
|
||||
('Hay Rack Feeder', 11.00, 5, 'Supplies for small pets'),
|
||||
('Wooden Chew Blocks', 27.00, 5, 'Supplies for small pets');
|
||||
|
||||
INSERT INTO productSupplier (supId, prodId, cost)
|
||||
SELECT CASE MOD(p.prodId - 7, 5)
|
||||
WHEN 0 THEN 1
|
||||
WHEN 1 THEN 2
|
||||
WHEN 2 THEN 3
|
||||
WHEN 3 THEN 4
|
||||
ELSE 5
|
||||
END,
|
||||
p.prodId,
|
||||
ROUND(p.prodPrice * (0.62 + (MOD(p.prodId - 7, 5) * 0.03)), 2)
|
||||
FROM product p
|
||||
WHERE p.prodId >= 7
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM productSupplier ps WHERE ps.prodId = p.prodId
|
||||
);
|
||||
|
||||
INSERT INTO inventory (prodId, quantity)
|
||||
SELECT p.prodId,
|
||||
CASE p.categoryId
|
||||
WHEN 1 THEN 120 + MOD((p.prodId - 7) * 17, 60)
|
||||
WHEN 2 THEN 180 + MOD((p.prodId - 7) * 17, 60)
|
||||
WHEN 3 THEN 70 + MOD((p.prodId - 7) * 17, 60)
|
||||
WHEN 4 THEN 45 + MOD((p.prodId - 7) * 17, 60)
|
||||
ELSE 95 + MOD((p.prodId - 7) * 17, 60)
|
||||
END
|
||||
FROM product p
|
||||
WHERE p.prodId >= 7
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM inventory i WHERE i.prodId = p.prodId
|
||||
);
|
||||
@@ -0,0 +1,191 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.entity.Appointment;
|
||||
import com.petshop.backend.entity.Customer;
|
||||
import com.petshop.backend.entity.Pet;
|
||||
import com.petshop.backend.entity.Service;
|
||||
import com.petshop.backend.entity.StoreLocation;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.AppointmentRepository;
|
||||
import com.petshop.backend.repository.CustomerRepository;
|
||||
import com.petshop.backend.repository.PetRepository;
|
||||
import com.petshop.backend.repository.ServiceRepository;
|
||||
import com.petshop.backend.repository.StoreRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AppointmentServiceTest {
|
||||
|
||||
@Mock
|
||||
private AppointmentRepository appointmentRepository;
|
||||
|
||||
@Mock
|
||||
private CustomerRepository customerRepository;
|
||||
|
||||
@Mock
|
||||
private PetRepository petRepository;
|
||||
|
||||
@Mock
|
||||
private ServiceRepository serviceRepository;
|
||||
|
||||
@Mock
|
||||
private StoreRepository storeRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private AppointmentService appointmentService;
|
||||
|
||||
private Customer customer;
|
||||
private StoreLocation store;
|
||||
private Service grooming;
|
||||
private Service nailTrim;
|
||||
private Pet pet;
|
||||
private LocalDate date;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
customer = new Customer();
|
||||
customer.setCustomerId(1L);
|
||||
customer.setFirstName("Pat");
|
||||
customer.setLastName("Owner");
|
||||
|
||||
store = new StoreLocation();
|
||||
store.setStoreId(1L);
|
||||
store.setStoreName("Main Store");
|
||||
|
||||
grooming = new Service();
|
||||
grooming.setServiceId(1L);
|
||||
grooming.setServiceName("Grooming");
|
||||
grooming.setServiceDuration(30);
|
||||
|
||||
nailTrim = new Service();
|
||||
nailTrim.setServiceId(2L);
|
||||
nailTrim.setServiceName("Nail Trim");
|
||||
nailTrim.setServiceDuration(30);
|
||||
|
||||
pet = new Pet();
|
||||
pet.setPetId(1L);
|
||||
pet.setPetName("Milo");
|
||||
|
||||
date = LocalDate.now().plusDays(1);
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAvailabilityAllowsDifferentServicesAtSameTime() {
|
||||
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
|
||||
|
||||
List<String> slots = appointmentService.checkAvailability(1L, 2L, date);
|
||||
|
||||
assertTrue(slots.contains("10:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAvailabilityBlocksSameServiceAtSameTime() {
|
||||
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
|
||||
|
||||
List<String> slots = appointmentService.checkAvailability(1L, 1L, date);
|
||||
|
||||
assertFalse(slots.contains("10:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelledAppointmentsDoNotBlockAvailability() {
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
|
||||
|
||||
List<String> slots = appointmentService.checkAvailability(1L, 1L, date);
|
||||
|
||||
assertTrue(slots.contains("10:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAppointmentDoesNotConflictWithItself() {
|
||||
Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store);
|
||||
User user = new User();
|
||||
user.setId(10L);
|
||||
user.setUsername("pat");
|
||||
user.setRole(User.Role.CUSTOMER);
|
||||
user.setTokenVersion(0);
|
||||
when(userRepository.findById(10L)).thenReturn(Optional.of(user));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0),
|
||||
"n/a",
|
||||
List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER"))
|
||||
)
|
||||
);
|
||||
|
||||
when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
|
||||
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
|
||||
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
|
||||
when(petRepository.findById(1L)).thenReturn(Optional.of(pet));
|
||||
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing));
|
||||
when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
|
||||
request.setCustomerId(1L);
|
||||
request.setStoreId(1L);
|
||||
request.setServiceId(1L);
|
||||
request.setAppointmentDate(date);
|
||||
request.setAppointmentTime(LocalTime.of(10, 0));
|
||||
request.setAppointmentStatus("Booked");
|
||||
request.setPetIds(List.of(1L));
|
||||
|
||||
var response = appointmentService.updateAppointment(1L, request);
|
||||
|
||||
assertEquals(1L, response.getAppointmentId());
|
||||
assertEquals("Booked", response.getAppointmentStatus());
|
||||
}
|
||||
|
||||
private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) {
|
||||
Appointment appointment = new Appointment();
|
||||
appointment.setAppointmentId(id);
|
||||
appointment.setAppointmentDate(date);
|
||||
appointment.setAppointmentTime(time);
|
||||
appointment.setAppointmentStatus("Booked");
|
||||
appointment.setService(service);
|
||||
appointment.setStore(storeLocation);
|
||||
appointment.setCustomer(customer);
|
||||
appointment.setPets(Set.of());
|
||||
return appointment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.dto.chat.MessageRequest;
|
||||
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||
import com.petshop.backend.entity.Conversation;
|
||||
import com.petshop.backend.entity.Customer;
|
||||
import com.petshop.backend.entity.Message;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.ConversationRepository;
|
||||
import com.petshop.backend.repository.CustomerRepository;
|
||||
import com.petshop.backend.repository.MessageRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChatServiceTest {
|
||||
|
||||
@Mock
|
||||
private ConversationRepository conversationRepository;
|
||||
|
||||
@Mock
|
||||
private MessageRepository messageRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private CustomerRepository customerRepository;
|
||||
|
||||
@InjectMocks
|
||||
private ChatService chatService;
|
||||
|
||||
private Customer customer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
customer = new Customer();
|
||||
customer.setCustomerId(1L);
|
||||
customer.setUserId(10L);
|
||||
customer.setFirstName("Pat");
|
||||
customer.setLastName("Owner");
|
||||
customer.setEmail("pat@example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateConversationMarksConversationClosed() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L))
|
||||
.thenReturn(List.of(message("hello")));
|
||||
|
||||
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"));
|
||||
|
||||
assertEquals("CLOSED", response.getStatus());
|
||||
assertEquals("hello", response.getLastMessage());
|
||||
verify(conversationRepository).save(conversation);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateConversationRejectsOtherCustomer() {
|
||||
Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||
|
||||
assertThrows(AccessDeniedException.class,
|
||||
() -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateConversationIsIdempotent() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||
|
||||
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"));
|
||||
|
||||
assertEquals("CLOSED", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffCanCloseAssignedConversation() {
|
||||
Conversation conversation = conversation(99L, 1L, 77L, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||
|
||||
var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED"));
|
||||
|
||||
assertEquals("CLOSED", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void staffCanCloseUnassignedConversation() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||
|
||||
var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED"));
|
||||
|
||||
assertEquals("CLOSED", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminCanCloseAnyConversation() {
|
||||
Conversation conversation = conversation(99L, 2L, 88L, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||
|
||||
var response = chatService.updateConversation(99L, 1L, User.Role.ADMIN, new UpdateConversationRequest("CLOSED"));
|
||||
|
||||
assertEquals("CLOSED", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateConversationCanReopenClosedConversation() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||
|
||||
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("OPEN"));
|
||||
|
||||
assertEquals("OPEN", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateConversationRejectsInvalidStatus() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("INVALID")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendMessageRejectsClosedConversation() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
|
||||
assertThrows(AccessDeniedException.class,
|
||||
() -> chatService.sendMessage(99L, 10L, User.Role.CUSTOMER, new MessageRequest("hello")));
|
||||
|
||||
verify(messageRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestHumanTakeoverRejectsClosedConversation() {
|
||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||
|
||||
assertThrows(AccessDeniedException.class,
|
||||
() -> chatService.requestHumanTakeover(99L, 10L, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
private Conversation conversation(Long id, Long customerId, Long staffId, Conversation.ConversationStatus status) {
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setId(id);
|
||||
conversation.setCustomerId(customerId);
|
||||
conversation.setStaffId(staffId);
|
||||
conversation.setStatus(status);
|
||||
conversation.setMode(Conversation.ConversationMode.AUTOMATED);
|
||||
conversation.setHumanRequestedAt(LocalDateTime.now());
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private Message message(String content) {
|
||||
Message message = new Message();
|
||||
message.setConversationId(99L);
|
||||
message.setSenderId(10L);
|
||||
message.setContent(content);
|
||||
message.setIsRead(false);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.dto.customer.CustomerRequest;
|
||||
import com.petshop.backend.entity.Customer;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.CustomerRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomerServiceTest {
|
||||
|
||||
@Mock
|
||||
private CustomerRepository customerRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Mock
|
||||
private UserBusinessLinkageService userBusinessLinkageService;
|
||||
|
||||
@InjectMocks
|
||||
private CustomerService customerService;
|
||||
|
||||
@Test
|
||||
void createCustomerCreatesLinkedUser() {
|
||||
CustomerRequest request = new CustomerRequest();
|
||||
request.setFirstName("Pat");
|
||||
request.setLastName("Owner");
|
||||
request.setEmail("pat@example.com");
|
||||
|
||||
Customer savedCustomer = new Customer();
|
||||
savedCustomer.setCustomerId(7L);
|
||||
savedCustomer.setFirstName("Pat");
|
||||
savedCustomer.setLastName("Owner");
|
||||
savedCustomer.setEmail("pat@example.com");
|
||||
|
||||
when(customerRepository.save(any(Customer.class))).thenReturn(savedCustomer);
|
||||
when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.empty());
|
||||
when(passwordEncoder.encode(any())).thenReturn("hashed-temp-password");
|
||||
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
|
||||
User user = invocation.getArgument(0);
|
||||
user.setId(11L);
|
||||
return user;
|
||||
});
|
||||
when(userBusinessLinkageService.ensureLinkedCustomer(any(User.class))).thenAnswer(invocation -> {
|
||||
User user = invocation.getArgument(0);
|
||||
savedCustomer.setUserId(user.getId());
|
||||
return savedCustomer;
|
||||
});
|
||||
|
||||
var response = customerService.createCustomer(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals("Pat", response.getFirstName());
|
||||
assertEquals("Owner", response.getLastName());
|
||||
assertEquals("pat@example.com", response.getEmail());
|
||||
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
verify(userRepository).save(userCaptor.capture());
|
||||
User createdUser = userCaptor.getValue();
|
||||
assertEquals("customer_7", createdUser.getUsername());
|
||||
assertEquals("hashed-temp-password", createdUser.getPassword());
|
||||
assertEquals("pat@example.com", createdUser.getEmail());
|
||||
assertEquals("Pat Owner", createdUser.getFullName());
|
||||
assertEquals("200-000-0007", createdUser.getPhone());
|
||||
assertEquals(false, createdUser.getActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCustomerRejectsExistingNonCustomerEmail() {
|
||||
CustomerRequest request = new CustomerRequest();
|
||||
request.setFirstName("Pat");
|
||||
request.setLastName("Owner");
|
||||
request.setEmail("pat@example.com");
|
||||
|
||||
User existing = new User();
|
||||
existing.setId(22L);
|
||||
existing.setUsername("staff1");
|
||||
existing.setEmail("pat@example.com");
|
||||
existing.setRole(User.Role.STAFF);
|
||||
|
||||
when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCustomerRejectsExistingCustomerEmail() {
|
||||
CustomerRequest request = new CustomerRequest();
|
||||
request.setFirstName("Pat");
|
||||
request.setLastName("Owner");
|
||||
request.setEmail("pat@example.com");
|
||||
|
||||
User existing = new User();
|
||||
existing.setId(22L);
|
||||
existing.setUsername("customer1");
|
||||
existing.setEmail("pat@example.com");
|
||||
existing.setRole(User.Role.CUSTOMER);
|
||||
|
||||
when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateCustomerRejectsExistingEmailFromOtherUser() {
|
||||
Customer customer = new Customer();
|
||||
customer.setCustomerId(7L);
|
||||
customer.setUserId(11L);
|
||||
customer.setFirstName("Pat");
|
||||
customer.setLastName("Owner");
|
||||
customer.setEmail("old@example.com");
|
||||
|
||||
CustomerRequest request = new CustomerRequest();
|
||||
request.setFirstName("Pat");
|
||||
request.setLastName("Owner");
|
||||
request.setEmail("pat@example.com");
|
||||
|
||||
User existing = new User();
|
||||
existing.setId(22L);
|
||||
existing.setUsername("customer2");
|
||||
existing.setEmail("pat@example.com");
|
||||
existing.setRole(User.Role.CUSTOMER);
|
||||
|
||||
when(customerRepository.findById(7L)).thenReturn(Optional.of(customer));
|
||||
when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteCustomerDeletesLinkedUser() {
|
||||
Customer customer = new Customer();
|
||||
customer.setCustomerId(7L);
|
||||
customer.setUserId(11L);
|
||||
|
||||
when(customerRepository.findById(7L)).thenReturn(Optional.of(customer));
|
||||
when(userRepository.existsById(11L)).thenReturn(true);
|
||||
|
||||
customerService.deleteCustomer(7L);
|
||||
|
||||
verify(userRepository).deleteById(11L);
|
||||
verify(customerRepository, never()).deleteById(7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteCustomerDeletesCustomerWhenNoLinkedUserExists() {
|
||||
Customer customer = new Customer();
|
||||
customer.setCustomerId(7L);
|
||||
customer.setUserId(11L);
|
||||
|
||||
when(customerRepository.findById(7L)).thenReturn(Optional.of(customer));
|
||||
when(userRepository.existsById(11L)).thenReturn(false);
|
||||
|
||||
customerService.deleteCustomer(7L);
|
||||
|
||||
verify(customerRepository).deleteById(7L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class SaleServiceTest {
|
||||
|
||||
@Test
|
||||
void normalizePaymentMethodMapsDebitToCard() {
|
||||
SaleService saleService = new SaleService(null, null, null, null, null, null, null, null);
|
||||
|
||||
assertEquals("Card", saleService.normalizePaymentMethod("Debit"));
|
||||
assertEquals("Card", saleService.normalizePaymentMethod("debit"));
|
||||
assertEquals("Cash", saleService.normalizePaymentMethod("Cash"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user