diff --git a/build.gradle b/build.gradle index d4fdc839..3cb83507 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,9 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Excel (Apache POI) + implementation 'org.apache.poi:poi-ooxml:5.2.3' } tasks.named('test') { diff --git a/generate_sample_excel.py b/generate_sample_excel.py new file mode 100644 index 00000000..cb4fbf2f --- /dev/null +++ b/generate_sample_excel.py @@ -0,0 +1,16 @@ +import pandas as pd + +# Create sample data +data = { + 'Product Code': ['P00000LM000D', 'P00000WA000B', 'P00000IP000A'], + 'Quantity': [10, 5, 20], + 'Applied At': ['2023-12-01 10:00:00', '2023-12-01 11:00:00', ''] +} + +# Create DataFrame +df = pd.DataFrame(data) + +# Save to Excel +df.to_excel('sample_retail.xlsx', index=False) + +print("Sample Excel file 'sample_retail.xlsx' created successfully.") diff --git a/sample_retail.xlsx b/sample_retail.xlsx new file mode 100644 index 00000000..732deae2 Binary files /dev/null and b/sample_retail.xlsx differ diff --git a/src/main/java/com/almang/inventory/customerorder/service/CustomerOrderService.java b/src/main/java/com/almang/inventory/customerorder/service/CustomerOrderService.java index 5a14ce37..771fa8ef 100644 --- a/src/main/java/com/almang/inventory/customerorder/service/CustomerOrderService.java +++ b/src/main/java/com/almang/inventory/customerorder/service/CustomerOrderService.java @@ -7,7 +7,6 @@ import com.almang.inventory.customerorder.repository.CustomerOrderRepository; import com.almang.inventory.global.exception.BaseException; import com.almang.inventory.global.exception.ErrorCode; -import com.almang.inventory.inventory.domain.Inventory; import com.almang.inventory.inventory.repository.InventoryRepository; import com.almang.inventory.product.domain.Product; import com.almang.inventory.product.repository.ProductRepository; @@ -16,12 +15,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - @Slf4j @Service @RequiredArgsConstructor @@ -37,7 +30,8 @@ public Long createCustomerOrderAndProcessStock(CustomerOrderRequest request) { // 1. 이미 존재하는 카페24 주문인지 확인 (중복 처리 방지) customerOrderRepository.findByCafe24OrderId(request.getCafe24OrderId()) .ifPresent(order -> { - throw new BaseException(ErrorCode.DUPLICATE_CUSTOMER_ORDER, "Cafe24 Order ID already exists: " + request.getCafe24OrderId()); + throw new BaseException(ErrorCode.DUPLICATE_CUSTOMER_ORDER, + "Cafe24 Order ID already exists: " + request.getCafe24OrderId()); }); // 2. CustomerOrder 엔티티 생성 @@ -46,7 +40,9 @@ public Long createCustomerOrderAndProcessStock(CustomerOrderRequest request) { .orderAt(request.getOrderAt()) .isPaid(request.getIsPaid().equalsIgnoreCase("T")) // 'T'/'F' 문자열을 boolean으로 변환 .isCanceled(request.getIsCanceled().equalsIgnoreCase("T")) // 'T'/'F' 문자열을 boolean으로 변환 - .paymentMethod(request.getPaymentMethodName() != null && !request.getPaymentMethodName().isEmpty() ? request.getPaymentMethodName().get(0) : null) + .paymentMethod(request.getPaymentMethodName() != null && !request.getPaymentMethodName().isEmpty() + ? request.getPaymentMethodName().get(0) + : null) .paymentAmount(request.getPaymentAmount()) .billingName(request.getBillingName()) .memberId(request.getMemberId()) @@ -58,20 +54,23 @@ public Long createCustomerOrderAndProcessStock(CustomerOrderRequest request) { // 3. CustomerOrderItem 처리 및 재고 처리 (감소 로직은 Placeholder) if (request.getItems() != null && !request.getItems().isEmpty()) { for (CustomerOrderItemRequest itemRequest : request.getItems()) { - // 3.1. 상품 조회 (productCode 사용) - Product product = productRepository.findByCode(itemRequest.getProductCode()) - .orElseThrow(() -> new BaseException(ErrorCode.PRODUCT_NOT_FOUND, "Product not found with code: " + itemRequest.getProductCode())); + // 3.1. 상품 조회 (cafe24Code 사용) + Product product = productRepository.findByCafe24Code(itemRequest.getProductCode()) + .orElseThrow(() -> new BaseException(ErrorCode.PRODUCT_NOT_FOUND, + "Product not found with code: " + itemRequest.getProductCode())); // 3.2. Inventory 조회 (상품과 연결된 재고 정보) - Inventory inventory = inventoryRepository.findByProduct(product) - .orElseThrow(() -> new BaseException(ErrorCode.INVENTORY_NOT_FOUND, "Inventory not found for product: " + product.getName())); + // Inventory inventory = inventoryRepository.findByProduct(product) + // .orElseThrow(() -> new BaseException(ErrorCode.INVENTORY_NOT_FOUND, + // "Inventory not found for product: " + product.getName())); + // TODO: 재고 조회 및 감소 로직 구현 필요 // 3.3. 재고 감소 로직 (Placeholder) - // ======================================================================== - // TODO: 카페24 연동 정책에 따라 정확한 재고 감소 로직을 여기에 구현해야 합니다. - // 예: inventory.decreaseWarehouse(new BigDecimal(itemRequest.getQuantity())); - // 현재는 재고 감소 로직이 적용되지 않습니다. - // ======================================================================== + // ======================================================================== + // TODO: 카페24 연동 정책에 따라 정확한 재고 감소 로직을 여기에 구현해야 합니다. + // 예: inventory.decreaseWarehouse(new BigDecimal(itemRequest.getQuantity())); + // 현재는 재고 감소 로직이 적용되지 않습니다. + // ======================================================================== log.warn("카페24 주문 ID {}의 상품 {} (수량 {})에 대한 재고 감소 로직이 정의되지 않았습니다.", request.getCafe24OrderId(), itemRequest.getProductName(), itemRequest.getQuantity()); diff --git a/src/main/java/com/almang/inventory/inventory/dto/response/InventoryResponse.java b/src/main/java/com/almang/inventory/inventory/dto/response/InventoryResponse.java index 7b48e227..8441d4f8 100644 --- a/src/main/java/com/almang/inventory/inventory/dto/response/InventoryResponse.java +++ b/src/main/java/com/almang/inventory/inventory/dto/response/InventoryResponse.java @@ -8,13 +8,13 @@ public record InventoryResponse( Long inventoryId, Long productId, String productName, - String productCode, + String cafe24Code, + String posCode, BigDecimal displayStock, BigDecimal warehouseStock, BigDecimal outgoingReserved, BigDecimal incomingReserved, - BigDecimal reorderTriggerPoint -) { + BigDecimal reorderTriggerPoint) { public static InventoryResponse from(Inventory inventory) { Product product = inventory.getProduct(); @@ -22,12 +22,12 @@ public static InventoryResponse from(Inventory inventory) { inventory.getId(), product.getId(), product.getName(), - product.getCode(), + product.getCafe24Code(), + product.getPosCode(), inventory.getDisplayStock(), inventory.getWarehouseStock(), inventory.getOutgoingReserved(), inventory.getIncomingReserved(), - inventory.getReorderTriggerPoint() - ); + inventory.getReorderTriggerPoint()); } } diff --git a/src/main/java/com/almang/inventory/product/domain/Product.java b/src/main/java/com/almang/inventory/product/domain/Product.java index 1c439a15..b5a2bb22 100644 --- a/src/main/java/com/almang/inventory/product/domain/Product.java +++ b/src/main/java/com/almang/inventory/product/domain/Product.java @@ -33,11 +33,14 @@ public class Product extends BaseTimeEntity { @JoinColumn(name = "vendor_id", nullable = false) private Vendor vendor; - @Column(name = "name", length = 30, nullable = false) + @Column(name = "name", length = 255, nullable = false) private String name; - @Column(name = "code", length = 30, nullable = false) - private String code; + @Column(name = "cafe24_code", length = 50, nullable = false) + private String cafe24Code; + + @Column(name = "pos_code", length = 50, nullable = false) + private String posCode; @Enumerated(EnumType.STRING) @Column(name = "unit", nullable = false) @@ -73,12 +76,15 @@ public void updateVendor(Vendor vendor) { } } - public void updateBasicInfo(String name, String code, ProductUnit unit) { + public void updateBasicInfo(String name, String cafe24Code, String posCode, ProductUnit unit) { if (name != null) { this.name = name; } - if (code != null) { - this.code = code; + if (cafe24Code != null) { + this.cafe24Code = cafe24Code; + } + if (posCode != null) { + this.posCode = posCode; } if (unit != null) { this.unit = unit; diff --git a/src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java b/src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java index 75da6c28..50b789c8 100644 --- a/src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java +++ b/src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java @@ -8,14 +8,15 @@ import java.math.BigDecimal; public record CreateProductRequest( - @NotNull Long vendorId, - @NotBlank String name, - @NotBlank String code, - @NotNull ProductUnit unit, - BigDecimal boxWeightG, - @Positive Integer unitPerBox, - BigDecimal unitWeightG, - @Min(0) Integer costPrice, - @Min(0) Integer retailPrice, - @Min(0) Integer wholesalePrice -) {} + @NotNull Long vendorId, + @NotBlank String name, + @NotBlank String cafe24Code, + @NotBlank String posCode, + @NotNull ProductUnit unit, + BigDecimal boxWeightG, + @Positive Integer unitPerBox, + BigDecimal unitWeightG, + @Min(0) Integer costPrice, + @Min(0) Integer retailPrice, + @Min(0) Integer wholesalePrice) { +} diff --git a/src/main/java/com/almang/inventory/product/dto/request/UpdateProductRequest.java b/src/main/java/com/almang/inventory/product/dto/request/UpdateProductRequest.java index 3f7088c2..5bb09852 100644 --- a/src/main/java/com/almang/inventory/product/dto/request/UpdateProductRequest.java +++ b/src/main/java/com/almang/inventory/product/dto/request/UpdateProductRequest.java @@ -7,15 +7,16 @@ import java.math.BigDecimal; public record UpdateProductRequest( - @NotNull Long vendorId, - String name, - String code, - ProductUnit unit, - BigDecimal boxWeightG, - @Positive Integer unitPerBox, - BigDecimal unitWeightG, - Boolean isActivated, - @Min(0) Integer costPrice, - @Min(0) Integer retailPrice, - @Min(0) Integer wholesalePrice -) {} + @NotNull Long vendorId, + String name, + String cafe24Code, + String posCode, + ProductUnit unit, + BigDecimal boxWeightG, + @Positive Integer unitPerBox, + BigDecimal unitWeightG, + Boolean isActivated, + @Min(0) Integer costPrice, + @Min(0) Integer retailPrice, + @Min(0) Integer wholesalePrice) { +} diff --git a/src/main/java/com/almang/inventory/product/dto/response/ProductResponse.java b/src/main/java/com/almang/inventory/product/dto/response/ProductResponse.java index eb005e7c..472e7bce 100644 --- a/src/main/java/com/almang/inventory/product/dto/response/ProductResponse.java +++ b/src/main/java/com/almang/inventory/product/dto/response/ProductResponse.java @@ -7,7 +7,8 @@ public record ProductResponse( Long productId, String name, - String code, + String cafe24Code, + String posCode, ProductUnit unit, BigDecimal boxWeightG, boolean isActivated, @@ -17,13 +18,13 @@ public record ProductResponse( int retailPrice, int wholesalePrice, Long storeId, - Long vendorId -) { + Long vendorId) { public static ProductResponse from(Product product) { return new ProductResponse( product.getId(), product.getName(), - product.getCode(), + product.getCafe24Code(), + product.getPosCode(), product.getUnit(), product.getBoxWeightG(), product.isActivated(), @@ -33,7 +34,6 @@ public static ProductResponse from(Product product) { product.getRetailPrice(), product.getWholesalePrice(), product.getStore().getId(), - product.getVendor().getId() - ); + product.getVendor().getId()); } } diff --git a/src/main/java/com/almang/inventory/product/repository/ProductRepository.java b/src/main/java/com/almang/inventory/product/repository/ProductRepository.java index efc9e830..61a89731 100644 --- a/src/main/java/com/almang/inventory/product/repository/ProductRepository.java +++ b/src/main/java/com/almang/inventory/product/repository/ProductRepository.java @@ -9,24 +9,25 @@ public interface ProductRepository extends JpaRepository { - Page findAllByStoreId(Long storeId, Pageable pageable); + Page findAllByStoreId(Long storeId, Pageable pageable); - Page findAllByStoreIdAndActivatedTrue(Long storeId, Pageable pageable); + Page findAllByStoreIdAndActivatedTrue(Long storeId, Pageable pageable); - Page findAllByStoreIdAndActivatedFalse(Long storeId, Pageable pageable); + Page findAllByStoreIdAndActivatedFalse(Long storeId, Pageable pageable); - Page findAllByStoreIdAndNameContainingIgnoreCase(Long storeId, String name, Pageable pageable); + Page findAllByStoreIdAndNameContainingIgnoreCase(Long storeId, String name, Pageable pageable); - Page findAllByStoreIdAndActivatedTrueAndNameContainingIgnoreCase( - Long storeId, String name, Pageable pageable - ); + Page findAllByStoreIdAndActivatedTrueAndNameContainingIgnoreCase( + Long storeId, String name, Pageable pageable); - Page findAllByStoreIdAndActivatedFalseAndNameContainingIgnoreCase( - Long storeId, String name, Pageable pageable - ); + Page findAllByStoreIdAndActivatedFalseAndNameContainingIgnoreCase( + Long storeId, String name, Pageable pageable); - boolean existsByVendorId(Long vendorId); + boolean existsByVendorId(Long vendorId); - // 상품 코드로 상품 찾기 (카페24 주문 처리용) - Optional findByCode(String code); + // 상품 코드로 상품 찾기 (카페24 주문 처리용) + Optional findByCafe24Code(String cafe24Code); + + // POS 코드로 상품 찾기 (소매 판매 엑셀 업로드용) + Optional findByPosCode(String posCode); } diff --git a/src/main/java/com/almang/inventory/product/service/ProductService.java b/src/main/java/com/almang/inventory/product/service/ProductService.java index 9fe0dc2b..e9bee80f 100644 --- a/src/main/java/com/almang/inventory/product/service/ProductService.java +++ b/src/main/java/com/almang/inventory/product/service/ProductService.java @@ -60,7 +60,7 @@ public ProductResponse updateProduct(Long productId, UpdateProductRequest reques log.info("[ProductService] 품목 수정 요청 - userId: {}, productId: {}", user.getId(), product.getId()); product.updateVendor(vendor); - product.updateBasicInfo(request.name(), request.code(), request.unit()); + product.updateBasicInfo(request.name(), request.cafe24Code(), request.posCode(), request.unit()); product.updateWeights(request.boxWeightG(), request.unitPerBox(), request.unitWeightG()); product.updatePrices(request.costPrice(), request.retailPrice(), request.wholesalePrice()); product.updateActivation(request.isActivated()); @@ -96,8 +96,7 @@ public ProductResponse getProductDetail(Long productId, Long userId) { @Transactional(readOnly = true) public PageResponse getProductList( - Long userId, Integer page, Integer size, Boolean isActivate, String nameKeyword - ) { + Long userId, Integer page, Integer size, Boolean isActivate, String nameKeyword) { UserStoreContext context = userContextProvider.findUserAndStore(userId); Store store = context.store(); @@ -117,7 +116,8 @@ private Product toEntity(CreateProductRequest request, User user) { .store(user.getStore()) .vendor(vendor) .name(request.name()) - .code(request.code()) + .cafe24Code(request.cafe24Code()) + .posCode(request.posCode()) .unit(request.unit()) .boxWeightG(request.boxWeightG()) .unitPerBox(request.unitPerBox()) @@ -141,7 +141,7 @@ private Vendor findVendorByIdAndValidateAccess(Long vendorId, User user) { } private Product findProductById(Long id) { - Product product = productRepository.findById(id) + Product product = productRepository.findById(id) .orElseThrow(() -> new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); if (product.getDeletedAt() != null) { @@ -157,8 +157,7 @@ private void validateStoreAccess(Product product, User user) { } private Page findProductsByFilter( - Long storeId, Boolean isActivate, String nameKeyword, PageRequest pageable - ) { + Long storeId, Boolean isActivate, String nameKeyword, PageRequest pageable) { boolean hasName = nameKeyword != null && !nameKeyword.isBlank(); boolean filterActivate = isActivate != null; @@ -178,18 +177,15 @@ private Page findProductsByFilter( // 3) 이름 검색 if (!filterActivate) { return productRepository.findAllByStoreIdAndNameContainingIgnoreCase( - storeId, nameKeyword, pageable - ); + storeId, nameKeyword, pageable); } // 4) 활성 + 이름 검색 if (isActivate) { return productRepository.findAllByStoreIdAndActivatedTrueAndNameContainingIgnoreCase( - storeId, nameKeyword, pageable - ); + storeId, nameKeyword, pageable); } return productRepository.findAllByStoreIdAndActivatedFalseAndNameContainingIgnoreCase( - storeId, nameKeyword, pageable - ); + storeId, nameKeyword, pageable); } } diff --git a/src/main/java/com/almang/inventory/retail/controller/RetailController.java b/src/main/java/com/almang/inventory/retail/controller/RetailController.java new file mode 100644 index 00000000..0333b549 --- /dev/null +++ b/src/main/java/com/almang/inventory/retail/controller/RetailController.java @@ -0,0 +1,39 @@ +package com.almang.inventory.retail.controller; + +import com.almang.inventory.retail.service.RetailService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; + +@Tag(name = "Retail", description = "소매 판매 관리 API") +@RestController +@RequestMapping("/api/v1/retail") +@RequiredArgsConstructor +public class RetailController { + + private final RetailService retailService; + + @Operation(summary = "엑셀 파일 업로드", description = "엑셀 파일을 업로드하여 소매 판매 내역을 등록하고 재고를 차감합니다.") + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity uploadRetailExcel(@RequestPart("file") MultipartFile file) { + if (file.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "Please select a file to upload")); + } + + try { + retailService.processRetailExcel(file); + return ResponseEntity.ok(Map.of("success", true, "message", "Retail data processed successfully")); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(Map.of("success", false, "error", e.getMessage())); + } + } +} diff --git a/src/main/java/com/almang/inventory/retail/domain/Retail.java b/src/main/java/com/almang/inventory/retail/domain/Retail.java new file mode 100644 index 00000000..222506a8 --- /dev/null +++ b/src/main/java/com/almang/inventory/retail/domain/Retail.java @@ -0,0 +1,42 @@ +package com.almang.inventory.retail.domain; + +import com.almang.inventory.global.entity.BaseTimeEntity; +import com.almang.inventory.product.domain.Product; +import com.almang.inventory.store.domain.Store; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "retails") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Retail extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "retail_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) + private Store store; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @Column(name = "quantity", nullable = false) + private BigDecimal quantity; + + @Column(name = "applied_at") + private LocalDateTime appliedAt; + + public void updateQuantity(BigDecimal quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/almang/inventory/retail/repository/RetailRepository.java b/src/main/java/com/almang/inventory/retail/repository/RetailRepository.java new file mode 100644 index 00000000..401080eb --- /dev/null +++ b/src/main/java/com/almang/inventory/retail/repository/RetailRepository.java @@ -0,0 +1,7 @@ +package com.almang.inventory.retail.repository; + +import com.almang.inventory.retail.domain.Retail; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RetailRepository extends JpaRepository { +} diff --git a/src/main/java/com/almang/inventory/retail/service/RetailService.java b/src/main/java/com/almang/inventory/retail/service/RetailService.java new file mode 100644 index 00000000..f7cbc0c6 --- /dev/null +++ b/src/main/java/com/almang/inventory/retail/service/RetailService.java @@ -0,0 +1,126 @@ +package com.almang.inventory.retail.service; + +import com.almang.inventory.global.exception.BaseException; +import com.almang.inventory.global.exception.ErrorCode; +import com.almang.inventory.inventory.repository.InventoryRepository; +import com.almang.inventory.product.domain.Product; +import com.almang.inventory.product.repository.ProductRepository; +import com.almang.inventory.retail.domain.Retail; +import com.almang.inventory.retail.repository.RetailRepository; +import com.almang.inventory.store.domain.Store; +import com.almang.inventory.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RetailService { + + private final RetailRepository retailRepository; + private final ProductRepository productRepository; + private final StoreRepository storeRepository; + private final InventoryRepository inventoryRepository; + + @Transactional + public void processRetailExcel(MultipartFile file) { + // 1. 유일한 상점 조회 (현재 상점은 하나뿐이라고 가정) + Store store = storeRepository.findAll().stream().findFirst() + .orElseThrow(() -> new BaseException(ErrorCode.STORE_NOT_FOUND)); + + try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) { + Sheet sheet = workbook.getSheetAt(0); // 첫 번째 시트 사용 + List retails = new ArrayList<>(); + + // 헤더 행 스킵 (첫 번째 행이 헤더라고 가정) + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + if (row == null) + continue; + + // 엑셀 양식 가정: + // Cell 1: 상품 코드 (POS Code) + // Cell 3: 수량 (Quantity) + // Cell 4: 실매출 (Real Sales) - 현재 미사용 + + String posCode = getCellValueAsString(row.getCell(1)); + if (posCode == null || posCode.isEmpty()) + continue; + + BigDecimal quantity = getCellValueAsBigDecimal(row.getCell(3)); + if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) + continue; + + LocalDateTime appliedAt = LocalDateTime.now(); // 기본값 + + // 2. 상품 조회 + Product product = productRepository.findByPosCode(posCode) + .orElseThrow(() -> new BaseException(ErrorCode.PRODUCT_NOT_FOUND, + "POS code not found: " + posCode)); + + // 3. Retail 엔티티 생성 + Retail retail = Retail.builder() + .store(store) + .product(product) + .quantity(quantity) + .appliedAt(appliedAt) + .build(); + retails.add(retail); + + // 4. 재고 차감 (매대 재고 차감으로 가정) + inventoryRepository.findByProduct(product).ifPresent(inventory -> { + inventory.decreaseDisplay(quantity); + }); + } + + // 5. Retail 저장 + retailRepository.saveAll(retails); + + } catch (IOException e) { + log.error("Failed to parse Excel file", e); + throw new RuntimeException("Failed to parse Excel file", e); + } + } + + private String getCellValueAsString(Cell cell) { + if (cell == null) + return null; + + if (cell.getCellType() == CellType.STRING) { + return cell.getStringCellValue(); + } else if (cell.getCellType() == CellType.NUMERIC) { + return String.valueOf((long) cell.getNumericCellValue()); + } else { + return ""; + } + } + + private BigDecimal getCellValueAsBigDecimal(Cell cell) { + if (cell == null) + return BigDecimal.ZERO; + + if (cell.getCellType() == CellType.NUMERIC) { + return BigDecimal.valueOf(cell.getNumericCellValue()); + } else if (cell.getCellType() == CellType.STRING) { + try { + return new BigDecimal(cell.getStringCellValue()); + } catch (NumberFormatException e) { + return BigDecimal.ZERO; + } + } else { + return BigDecimal.ZERO; + } + } +} diff --git a/src/test/java/com/almang/inventory/inventory/controller/InventoryControllerTest.java b/src/test/java/com/almang/inventory/inventory/controller/InventoryControllerTest.java index 7dedae37..b0f630df 100644 --- a/src/test/java/com/almang/inventory/inventory/controller/InventoryControllerTest.java +++ b/src/test/java/com/almang/inventory/inventory/controller/InventoryControllerTest.java @@ -44,447 +44,450 @@ @ActiveProfiles("test") public class InventoryControllerTest { - @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - - @MockitoBean private InventoryService inventoryService; - @MockitoBean private JpaMetamodelMappingContext jpaMetamodelMappingContext; - @MockitoBean private DiscordErrorNotifier discordErrorNotifier; - - private UsernamePasswordAuthenticationToken auth() { - CustomUserPrincipal principal = - new CustomUserPrincipal(1L, "inventory_admin", List.of()); - return new UsernamePasswordAuthenticationToken( - principal, null, principal.getAuthorities() - ); - } - - @Test - void 재고_수동_수정에_성공한다() throws Exception { - // given - Long inventoryId = 1L; - Long productId = 10L; - - UpdateInventoryRequest request = new UpdateInventoryRequest( - productId, - BigDecimal.valueOf(1.234), - BigDecimal.valueOf(10.000), - BigDecimal.valueOf(0.500), - BigDecimal.valueOf(3.000), - BigDecimal.valueOf(0.25) - ); - - InventoryResponse response = new InventoryResponse( - inventoryId, - productId, - "상품1", - "P001", - BigDecimal.valueOf(1.234), - BigDecimal.valueOf(10.000), - BigDecimal.valueOf(0.500), - BigDecimal.valueOf(3.000), - BigDecimal.valueOf(0.25) - ); - - when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.UPDATE_INVENTORY_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) - .andExpect(jsonPath("$.data.productId").value(productId)) - .andExpect(jsonPath("$.data.displayStock").value(1.234)) - .andExpect(jsonPath("$.data.warehouseStock").value(10.000)) - .andExpect(jsonPath("$.data.incomingReserved").value(3.000)) - .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.25)); - } - - @Test - void 재고_수동_수정시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 1L; - - UpdateInventoryRequest request = new UpdateInventoryRequest( - 10L, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(0.2) - ); - - when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_수동_수정시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 9999L; - - UpdateInventoryRequest request = new UpdateInventoryRequest( - 10L, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(0.2) - ); - - when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); - - // when & then - mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_수동_수정_요청값_검증에_실패하면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 1L; - - UpdateInventoryRequest invalidRequest = new UpdateInventoryRequest( - null, - BigDecimal.valueOf(-1.0), - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(1.5) - ); - - // when & then - mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_ID_기반_재고_조회에_성공한다() throws Exception { - // given - Long inventoryId = 1L; - Long productId = 10L; - - InventoryResponse response = new InventoryResponse( - inventoryId, - productId, - "상품1", - "P001", - BigDecimal.valueOf(1.234), - BigDecimal.valueOf(10.000), - BigDecimal.valueOf(0.500), - BigDecimal.valueOf(3.000), - BigDecimal.valueOf(0.25) - ); - - when(inventoryService.getInventory(anyLong(), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.GET_INVENTORY_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) - .andExpect(jsonPath("$.data.productId").value(productId)) - .andExpect(jsonPath("$.data.displayStock").value(1.234)) - .andExpect(jsonPath("$.data.warehouseStock").value(10.000)) - .andExpect(jsonPath("$.data.incomingReserved").value(3.000)) - .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.25)); - } - - @Test - void 재고_ID_기반_재고_조회시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 1L; - - when(inventoryService.getInventory(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_ID_기반_재고_조회시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 9999L; - - when(inventoryService.getInventory(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_ID_기반_재고_조회에_성공한다() throws Exception { - // given - Long inventoryId = 1L; - Long productId = 10L; - - InventoryResponse response = new InventoryResponse( - inventoryId, - productId, - "상품1", - "P001", - BigDecimal.valueOf(1.000), - BigDecimal.valueOf(5.000), - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(0.3) - ); - - when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.GET_INVENTORY_BY_PRODUCT_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) - .andExpect(jsonPath("$.data.productId").value(productId)) - .andExpect(jsonPath("$.data.displayStock").value(1.000)) - .andExpect(jsonPath("$.data.warehouseStock").value(5.000)) - .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.3)); - } - - @Test - void 품목_ID_기반_재고_조회시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long productId = 10L; - - when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_ID_기반_재고_조회시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long productId = 9999L; - - when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 상점_재고_목록_조회에_성공한다() throws Exception { - // given - Long inventoryId = 1L; - Long productId = 10L; - - InventoryResponse inventoryResponse = new InventoryResponse( - inventoryId, - productId, - "상품1", - "P001", - BigDecimal.ONE, - BigDecimal.TEN, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(0.2) - ); - - Page page = new PageImpl<>( - List.of(inventoryResponse), - PageRequest.of(0, 20), - 1 - ); - - PageResponse pageResponse = PageResponse.from(page); - - when(inventoryService.getStoreInventoryList(anyLong(), anyInt(), anyInt(), any(), any(), any())) - .thenReturn(pageResponse); - - // when & then - mockMvc.perform(get("/api/v1/inventory") - .param("page", "0") - .param("size", "20") - .param("scope", "all") - .param("q", "") - .param("sort", "") - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message").value(SuccessMessage.GET_STORE_INVENTORY_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.content[0].inventoryId").value(inventoryId)) - .andExpect(jsonPath("$.data.content[0].productId").value(productId)) - .andExpect(jsonPath("$.data.content[0].productName").value("상품1")) - .andExpect(jsonPath("$.data.content[0].productCode").value("P001")); - } - - @Test - void 재고_이동에_성공한다() throws Exception { - // given - Long inventoryId = 1L; - Long productId = 10L; - - MoveInventoryRequest request = new MoveInventoryRequest( - BigDecimal.valueOf(3), InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - - InventoryResponse response = new InventoryResponse( - inventoryId, - productId, - "상품1", - "P001", - BigDecimal.valueOf(3), - BigDecimal.valueOf(7), - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.valueOf(0.2) - ); - - when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message").value(SuccessMessage.MOVE_INVENTORY_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) - .andExpect(jsonPath("$.data.productId").value(productId)) - .andExpect(jsonPath("$.data.productName").value("상품1")) - .andExpect(jsonPath("$.data.productCode").value("P001")) - .andExpect(jsonPath("$.data.displayStock").value(3)) - .andExpect(jsonPath("$.data.warehouseStock").value(7)); - } - - @Test - void 재고_이동시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 1L; - - MoveInventoryRequest request = new MoveInventoryRequest( - BigDecimal.ONE, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - - when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_이동시_재고_도메인_예외가_발생하면_에러_응답을_반환한다() throws Exception { - // given - Long inventoryId = 1L; - - MoveInventoryRequest request = new MoveInventoryRequest( - BigDecimal.TEN, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - - when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH)); - - // when & then - mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status").value(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 재고_이동_요청값_검증에_실패하면_예외가_발생한다() throws Exception { - // given - Long inventoryId = 1L; - - MoveInventoryRequest invalidRequest = new MoveInventoryRequest( - BigDecimal.valueOf(-1), null - ); - - // when & then - mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status").value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private InventoryService inventoryService; + @MockitoBean + private JpaMetamodelMappingContext jpaMetamodelMappingContext; + @MockitoBean + private DiscordErrorNotifier discordErrorNotifier; + + private UsernamePasswordAuthenticationToken auth() { + CustomUserPrincipal principal = new CustomUserPrincipal(1L, "inventory_admin", List.of()); + return new UsernamePasswordAuthenticationToken( + principal, null, principal.getAuthorities()); + } + + @Test + void 재고_수동_수정에_성공한다() throws Exception { + // given + Long inventoryId = 1L; + Long productId = 10L; + + UpdateInventoryRequest request = new UpdateInventoryRequest( + productId, + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(10.000), + BigDecimal.valueOf(0.500), + BigDecimal.valueOf(3.000), + BigDecimal.valueOf(0.25)); + + InventoryResponse response = new InventoryResponse( + inventoryId, + productId, + "상품1", + "C001", + "P001", + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(10.000), + BigDecimal.valueOf(0.500), + BigDecimal.valueOf(3.000), + BigDecimal.valueOf(0.25)); + + when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.UPDATE_INVENTORY_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) + .andExpect(jsonPath("$.data.productId").value(productId)) + .andExpect(jsonPath("$.data.displayStock").value(1.234)) + .andExpect(jsonPath("$.data.warehouseStock").value(10.000)) + .andExpect(jsonPath("$.data.incomingReserved").value(3.000)) + .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.25)); + } + + @Test + void 재고_수동_수정시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 1L; + + UpdateInventoryRequest request = new UpdateInventoryRequest( + 10L, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(0.2)); + + when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_수동_수정시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 9999L; + + UpdateInventoryRequest request = new UpdateInventoryRequest( + 10L, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(0.2)); + + when(inventoryService.updateInventory(anyLong(), any(UpdateInventoryRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); + + // when & then + mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_수동_수정_요청값_검증에_실패하면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 1L; + + UpdateInventoryRequest invalidRequest = new UpdateInventoryRequest( + null, + BigDecimal.valueOf(-1.0), + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(1.5)); + + // when & then + mockMvc.perform(patch("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_ID_기반_재고_조회에_성공한다() throws Exception { + // given + Long inventoryId = 1L; + Long productId = 10L; + + InventoryResponse response = new InventoryResponse( + inventoryId, + productId, + "상품1", + "C001", + "P001", + BigDecimal.valueOf(1.234), + BigDecimal.valueOf(10.000), + BigDecimal.valueOf(0.500), + BigDecimal.valueOf(3.000), + BigDecimal.valueOf(0.25)); + + when(inventoryService.getInventory(anyLong(), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_INVENTORY_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) + .andExpect(jsonPath("$.data.productId").value(productId)) + .andExpect(jsonPath("$.data.displayStock").value(1.234)) + .andExpect(jsonPath("$.data.warehouseStock").value(10.000)) + .andExpect(jsonPath("$.data.incomingReserved").value(3.000)) + .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.25)); + } + + @Test + void 재고_ID_기반_재고_조회시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 1L; + + when(inventoryService.getInventory(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_ID_기반_재고_조회시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 9999L; + + when(inventoryService.getInventory(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/inventory/{inventoryId}", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_ID_기반_재고_조회에_성공한다() throws Exception { + // given + Long inventoryId = 1L; + Long productId = 10L; + + InventoryResponse response = new InventoryResponse( + inventoryId, + productId, + "상품1", + "C001", + "P001", + BigDecimal.valueOf(1.000), + BigDecimal.valueOf(5.000), + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(0.3)); + + when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_INVENTORY_BY_PRODUCT_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) + .andExpect(jsonPath("$.data.productId").value(productId)) + .andExpect(jsonPath("$.data.displayStock").value(1.000)) + .andExpect(jsonPath("$.data.warehouseStock").value(5.000)) + .andExpect(jsonPath("$.data.reorderTriggerPoint").value(0.3)); + } + + @Test + void 품목_ID_기반_재고_조회시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long productId = 10L; + + when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_ID_기반_재고_조회시_재고가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long productId = 9999L; + + when(inventoryService.getInventoryByProduct(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.INVENTORY_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/inventory/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVENTORY_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.INVENTORY_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 상점_재고_목록_조회에_성공한다() throws Exception { + // given + Long inventoryId = 1L; + Long productId = 10L; + + InventoryResponse inventoryResponse = new InventoryResponse( + inventoryId, + productId, + "상품1", + "C001", + "P001", + BigDecimal.ONE, + BigDecimal.TEN, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(0.2)); + + Page page = new PageImpl<>( + List.of(inventoryResponse), + PageRequest.of(0, 20), + 1); + + PageResponse pageResponse = PageResponse.from(page); + + when(inventoryService.getStoreInventoryList(anyLong(), anyInt(), anyInt(), any(), any(), any())) + .thenReturn(pageResponse); + + // when & then + mockMvc.perform(get("/api/v1/inventory") + .param("page", "0") + .param("size", "20") + .param("scope", "all") + .param("q", "") + .param("sort", "") + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_STORE_INVENTORY_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.content[0].inventoryId").value(inventoryId)) + .andExpect(jsonPath("$.data.content[0].productId").value(productId)) + .andExpect(jsonPath("$.data.content[0].productName").value("상품1")) + .andExpect(jsonPath("$.data.content[0].cafe24Code").value("C001")) + .andExpect(jsonPath("$.data.content[0].posCode").value("P001")); + } + + @Test + void 재고_이동에_성공한다() throws Exception { + // given + Long inventoryId = 1L; + Long productId = 10L; + + MoveInventoryRequest request = new MoveInventoryRequest( + BigDecimal.valueOf(3), InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + + InventoryResponse response = new InventoryResponse( + inventoryId, + productId, + "상품1", + "C001", + "P001", + BigDecimal.valueOf(3), + BigDecimal.valueOf(7), + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.valueOf(0.2)); + + when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.MOVE_INVENTORY_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.inventoryId").value(inventoryId)) + .andExpect(jsonPath("$.data.productId").value(productId)) + .andExpect(jsonPath("$.data.productName").value("상품1")) + .andExpect(jsonPath("$.data.cafe24Code").value("C001")) + .andExpect(jsonPath("$.data.posCode").value("P001")) + .andExpect(jsonPath("$.data.displayStock").value(3)) + .andExpect(jsonPath("$.data.warehouseStock").value(7)); + } + + @Test + void 재고_이동시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 1L; + + MoveInventoryRequest request = new MoveInventoryRequest( + BigDecimal.ONE, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + + when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_이동시_재고_도메인_예외가_발생하면_에러_응답을_반환한다() throws Exception { + // given + Long inventoryId = 1L; + + MoveInventoryRequest request = new MoveInventoryRequest( + BigDecimal.TEN, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + + when(inventoryService.moveInventory(anyLong(), any(MoveInventoryRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH)); + + // when & then + mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 재고_이동_요청값_검증에_실패하면_예외가_발생한다() throws Exception { + // given + Long inventoryId = 1L; + + MoveInventoryRequest invalidRequest = new MoveInventoryRequest( + BigDecimal.valueOf(-1), null); + + // when & then + mockMvc.perform(post("/api/v1/inventory/{inventoryId}/move", inventoryId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } } diff --git a/src/test/java/com/almang/inventory/inventory/service/InventoryServiceTest.java b/src/test/java/com/almang/inventory/inventory/service/InventoryServiceTest.java index e42404ec..118165d2 100644 --- a/src/test/java/com/almang/inventory/inventory/service/InventoryServiceTest.java +++ b/src/test/java/com/almang/inventory/inventory/service/InventoryServiceTest.java @@ -35,644 +35,637 @@ @ActiveProfiles("test") class InventoryServiceTest { - @Autowired private InventoryService inventoryService; - @Autowired private InventoryRepository inventoryRepository; - @Autowired private StoreRepository storeRepository; - @Autowired private UserRepository userRepository; - @Autowired private VendorRepository vendorRepository; - @Autowired private ProductRepository productRepository; - - private Store newStore(String name) { - return storeRepository.save( - Store.builder() - .name(name) - .isActivate(true) - .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) - .build() - ); - } - - private User newUser(Store store, String username) { - return userRepository.save( - User.builder() - .store(store) - .username(username) - .password("encoded-password") - .name("테스트 유저") - .role(UserRole.ADMIN) - .build() - ); - } - - private Vendor newVendor(Store store, String name) { - return vendorRepository.save( - Vendor.builder() - .store(store) - .name(name) - .channel(VendorChannel.KAKAO) - .contactPoint("010-1111-1111") - .note("비고") - .activated(true) - .build() - ); - } - - private Product newProduct(Store store, Vendor vendor, String name, String code) { - return productRepository.save( - Product.builder() - .store(store) - .vendor(vendor) - .name(name) - .code(code) - .unit(ProductUnit.EA) - .boxWeightG(null) - .unitPerBox(null) - .unitWeightG(null) - .activated(true) - .costPrice(1000) - .retailPrice(1500) - .wholesalePrice(1200) - .build() - ); - } - - @Test - void 재고_수동_수정에_성공한다() { - // given - Store store = newStore("수동수정상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - Product product = newProduct(store, vendor, "상품1", "P001"); - - inventoryService.createInventory(product); - - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - BigDecimal newDisplay = BigDecimal.valueOf(1.234); - BigDecimal newWarehouse = BigDecimal.valueOf(10.000); - BigDecimal newOutgoing = BigDecimal.valueOf(0.500); - BigDecimal newIncoming = BigDecimal.valueOf(3.000); - BigDecimal newReorderTrigger = BigDecimal.valueOf(0.25); - - UpdateInventoryRequest request = new UpdateInventoryRequest( - product.getId(), - newDisplay, - newWarehouse, - newOutgoing, - newIncoming, - newReorderTrigger - ); - - // when - InventoryResponse response = inventoryService.updateInventory(inventory.getId(), request, user.getId()); - - // then - assertThat(response).isNotNull(); - assertThat(response.inventoryId()).isEqualTo(inventory.getId()); - assertThat(response.productId()).isEqualTo(product.getId()); - assertThat(response.productName()).isEqualTo(product.getName()); - assertThat(response.productCode()).isEqualTo(product.getCode()); - assertThat(response.displayStock()).isEqualByComparingTo(newDisplay); - assertThat(response.warehouseStock()).isEqualByComparingTo(newWarehouse); - assertThat(response.outgoingReserved()).isEqualByComparingTo(newOutgoing); - assertThat(response.incomingReserved()).isEqualByComparingTo(newIncoming); - assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(newReorderTrigger); - - Inventory updated = inventoryRepository.findById(inventory.getId()) - .orElseThrow(); - assertThat(updated.getDisplayStock()).isEqualByComparingTo(newDisplay); - assertThat(updated.getWarehouseStock()).isEqualByComparingTo(newWarehouse); - assertThat(updated.getOutgoingReserved()).isEqualByComparingTo(newOutgoing); - assertThat(updated.getIncomingReserved()).isEqualByComparingTo(newIncoming); - assertThat(updated.getReorderTriggerPoint()).isEqualByComparingTo(newReorderTrigger); - } - - @Test - void 재고_수동_수정시_품목_아이디가_다르면_예외가_발생한다() { - // given - Store store = newStore("상품불일치상점"); - User user = newUser(store, "mismatchUser"); - Vendor vendor = newVendor(store, "발주처"); - - Product product1 = newProduct(store, vendor, "상품1", "P001"); - Product product2 = newProduct(store, vendor, "상품2", "P002"); - - inventoryService.createInventory(product1); - Inventory inventory = inventoryRepository.findByProduct_Id(product1.getId()) - .orElseThrow(); - - UpdateInventoryRequest request = new UpdateInventoryRequest( - product2.getId(), - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - - // when & then - assertThatThrownBy(() -> inventoryService.updateInventory(inventory.getId(), request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_PRODUCT_MISMATCH.getMessage()); - } - - @Test - void 재고_수동_수정시_다른_상점의_재고면_접근_거부_예외가_발생한다() { - // given - Store store1 = newStore("상점1"); - Store store2 = newStore("상점2"); - - User user1 = newUser(store1, "user1"); - Vendor vendor2 = newVendor(store2, "발주처2"); - Product product2 = newProduct(store2, vendor2, "상품2", "P002"); - - inventoryService.createInventory(product2); - Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) - .orElseThrow(); - - UpdateInventoryRequest request = new UpdateInventoryRequest( - product2.getId(), - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - - // when & then - assertThatThrownBy(() -> inventoryService.updateInventory(inventoryOfStore2.getId(), request, user1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); - } - - @Test - void 재고_수동_수정시_사용자가_존재하지_않으면_예외가_발생한다() { - // given - Long notExistUserId = 9999L; - Long anyInventoryId = 1L; - - UpdateInventoryRequest request = new UpdateInventoryRequest( - null, - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - - // when & then - assertThatThrownBy(() -> inventoryService.updateInventory(anyInventoryId, request, notExistUserId)) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); - } - - @Test - void 재고_수동_수정시_재고가_존재하지_않으면_예외가_발생한다() { - // given - Store store = newStore("재고없음상점"); - User user = newUser(store, "noInventoryUser"); - Long notExistInventoryId = 9999L; - - UpdateInventoryRequest request = new UpdateInventoryRequest( - null, - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - - // when & then - assertThatThrownBy(() -> inventoryService.updateInventory(notExistInventoryId, request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_NOT_FOUND.getMessage()); - } - - @Test - void 재고_ID로_재고_조회에_성공한다() { - // given - Store store = newStore("조회상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - Product product = newProduct(store, vendor, "상품1", "P001"); - - inventoryService.createInventory(product); - - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - // when - InventoryResponse response = inventoryService.getInventory(inventory.getId(), user.getId()); - - // then - assertThat(response).isNotNull(); - assertThat(response.inventoryId()).isEqualTo(inventory.getId()); - assertThat(response.productId()).isEqualTo(product.getId()); - assertThat(response.productName()).isEqualTo(product.getName()); - assertThat(response.productCode()).isEqualTo(product.getCode()); - assertThat(response.displayStock()).isEqualByComparingTo(inventory.getDisplayStock()); - assertThat(response.warehouseStock()).isEqualByComparingTo(inventory.getWarehouseStock()); - assertThat(response.outgoingReserved()).isEqualByComparingTo(inventory.getOutgoingReserved()); - assertThat(response.incomingReserved()).isEqualByComparingTo(inventory.getIncomingReserved()); - assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(inventory.getReorderTriggerPoint()); - - } - - @Test - void 재고_ID로_재고_조회시_다른_상점의_재고면_접근_거부_예외가_발생한다() { - // given - Store store1 = newStore("상점1"); - Store store2 = newStore("상점2"); - - User user1 = newUser(store1, "user1"); - Vendor vendor2 = newVendor(store2, "발주처2"); - Product product2 = newProduct(store2, vendor2, "상품2", "P002"); - - inventoryService.createInventory(product2); - Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) - .orElseThrow(); - - // when & then - assertThatThrownBy(() -> inventoryService.getInventory(inventoryOfStore2.getId(), user1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); - } - - @Test - void 품목으로_재고_조회에_성공한다() { - // given - Store store = newStore("품목조회상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - Product product = newProduct(store, vendor, "상품1", "P001"); - - inventoryService.createInventory(product); - - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - // when - InventoryResponse response = inventoryService.getInventoryByProduct(product.getId(), user.getId()); - - // then - assertThat(response).isNotNull(); - assertThat(response.inventoryId()).isEqualTo(inventory.getId()); - assertThat(response.productId()).isEqualTo(product.getId()); - assertThat(response.productName()).isEqualTo(product.getName()); - assertThat(response.productCode()).isEqualTo(product.getCode()); - assertThat(response.displayStock()).isEqualByComparingTo(inventory.getDisplayStock()); - assertThat(response.warehouseStock()).isEqualByComparingTo(inventory.getWarehouseStock()); - assertThat(response.outgoingReserved()).isEqualByComparingTo(inventory.getOutgoingReserved()); - assertThat(response.incomingReserved()).isEqualByComparingTo(inventory.getIncomingReserved()); - assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(inventory.getReorderTriggerPoint()); - } - - @Test - void 품목으로_재고_조회시_다른_상점의_재고면_접근_거부_예외가_발생한다() { - // given - Store store1 = newStore("상점1"); - Store store2 = newStore("상점2"); - - User user1 = newUser(store1, "user1"); - Vendor vendor2 = newVendor(store2, "발주처2"); - Product product2 = newProduct(store2, vendor2, "상품2", "P002"); - - inventoryService.createInventory(product2); - - // when & then - assertThatThrownBy(() -> inventoryService.getInventoryByProduct(product2.getId(), user1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); - } - - @Test - void 상점_재고_전체_조회에_성공한다() { - // given - Store store = newStore("재고목록상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - - Product product1 = newProduct(store, vendor, "상품A", "P001"); - Product product2 = newProduct(store, vendor, "상품B", "P002"); - Product product3 = newProduct(store, vendor, "상품C", "P003"); - - inventoryService.createInventory(product1); - inventoryService.createInventory(product2); - inventoryService.createInventory(product3); - - Store otherStore = newStore("다른상점"); - Vendor otherVendor = newVendor(otherStore, "다른발주처"); - Product otherProduct = newProduct(otherStore, otherVendor, "다른상품", "PX01"); - inventoryService.createInventory(otherProduct); - - // when - PageResponse pageResponse = - inventoryService.getStoreInventoryList(user.getId(), 0, 20, "all", null, null); - - // then - assertThat(pageResponse).isNotNull(); - assertThat(pageResponse.content()).hasSize(3); - - assertThat(pageResponse.content()) - .extracting(InventoryResponse::productId) - .containsExactlyInAnyOrder( - product1.getId(), - product2.getId(), - product3.getId() - ); - - assertThat(pageResponse.content()) - .extracting(InventoryResponse::productName) - .containsExactlyInAnyOrder("상품A", "상품B", "상품C"); - } - - @Test - void 상점_재고_목록_조회시_검색어로_필터링된다() { - // given - Store store = newStore("검색상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - - Product target = newProduct(store, vendor, "고체치약 60ea", "P001"); - Product other1 = newProduct(store, vendor, "고무장갑 M", "P002"); - Product other2 = newProduct(store, vendor, "실리콘 용기 540ml", "P003"); - - inventoryService.createInventory(target); - inventoryService.createInventory(other1); - inventoryService.createInventory(other2); - - // when - PageResponse pageResponse = - inventoryService.getStoreInventoryList(user.getId(), 0, 20, "all", "치약", null); - - // then - assertThat(pageResponse).isNotNull(); - assertThat(pageResponse.content()).hasSize(1); - - InventoryResponse result = pageResponse.content().get(0); - assertThat(result.productId()).isEqualTo(target.getId()); - assertThat(result.productName()).isEqualTo("고체치약 60ea"); - } - - @Test - void 상점_재고_목록_조회시_scope가_적용된다() { - // given - Store store = newStore("스코프상점"); - User user = newUser(store, "inventoryUser"); - Vendor vendor = newVendor(store, "발주처"); - - Product displayProduct = newProduct(store, vendor, "매대상품", "P001"); - Product warehouseProduct = newProduct(store, vendor, "창고상품", "P002"); - - inventoryService.createInventory(displayProduct); - inventoryService.createInventory(warehouseProduct); - - Inventory displayInventory = inventoryRepository.findByProduct_Id(displayProduct.getId()) - .orElseThrow(); - Inventory warehouseInventory = inventoryRepository.findByProduct_Id(warehouseProduct.getId()) - .orElseThrow(); - - // 매대상품: display_stock > 0 - UpdateInventoryRequest displayUpdate = new UpdateInventoryRequest( - displayProduct.getId(), - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(displayInventory.getId(), displayUpdate, user.getId()); - - // 창고상품: warehouse_stock > 0, display_stock = 0 - UpdateInventoryRequest warehouseUpdate = new UpdateInventoryRequest( - warehouseProduct.getId(), - BigDecimal.ZERO, - BigDecimal.TEN, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(warehouseInventory.getId(), warehouseUpdate, user.getId()); - - // when: scope = display - PageResponse pageResponse = - inventoryService.getStoreInventoryList(user.getId(), 0, 20, "display", null, null); - - // then - assertThat(pageResponse).isNotNull(); - assertThat(pageResponse.content()).hasSize(1); - - InventoryResponse only = pageResponse.content().get(0); - assertThat(only.productId()).isEqualTo(displayProduct.getId()); - assertThat(only.productName()).isEqualTo("매대상품"); - } - - @Test - void 상점_재고_목록_조회시_상품명_기준_오름차순_정렬이_적용된다() { - // given - Store store = newStore("정렬상점"); - User user = newUser(store, "sortUser"); - Vendor vendor = newVendor(store, "발주처"); - - Product p3 = newProduct(store, vendor, "치약", "P003"); - Product p1 = newProduct(store, vendor, "고무장갑", "P001"); - Product p2 = newProduct(store, vendor, "실리콘 용기", "P002"); - - inventoryService.createInventory(p3); - inventoryService.createInventory(p1); - inventoryService.createInventory(p2); - - // when: sort=productName - PageResponse response = - inventoryService.getStoreInventoryList(user.getId(), 0, 20, "all", null, "productName"); - - // then - assertThat(response).isNotNull(); - assertThat(response.content()).hasSize(3); - - // 오름차순 정렬 결과 체크 - assertThat(response.content()) - .extracting(InventoryResponse::productName) - .containsExactly("고무장갑", "실리콘 용기", "치약"); - } - - @Test - void 창고에서_매대로_재고_이동에_성공한다() { - // given - Store store = newStore("이동상점"); - User user = newUser(store, "moveUser"); - Vendor vendor = newVendor(store, "발주처"); - Product product = newProduct(store, vendor, "이동상품", "P001"); - - inventoryService.createInventory(product); - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - UpdateInventoryRequest initRequest = new UpdateInventoryRequest( - product.getId(), - BigDecimal.ZERO, - BigDecimal.TEN, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); - - // when - MoveInventoryRequest moveRequest = new MoveInventoryRequest( - BigDecimal.valueOf(3), - InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - InventoryResponse response = inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId()); - - // then - assertThat(response.displayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); - assertThat(response.warehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(7)); - - Inventory updated = inventoryRepository.findById(inventory.getId()) - .orElseThrow(); - assertThat(updated.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); - assertThat(updated.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(7)); - } - - @Test - void 매대에서_창고로_재고_이동에_성공한다() { - // given - Store store = newStore("이동상점2"); - User user = newUser(store, "moveUser2"); - Vendor vendor = newVendor(store, "발주처2"); - Product product = newProduct(store, vendor, "이동상품2", "P002"); - - inventoryService.createInventory(product); - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - UpdateInventoryRequest initRequest = new UpdateInventoryRequest( - product.getId(), - BigDecimal.valueOf(5), - BigDecimal.valueOf(2), - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); - - // when - MoveInventoryRequest moveRequest = new MoveInventoryRequest( - BigDecimal.valueOf(2), - InventoryMoveDirection.DISPLAY_TO_WAREHOUSE - ); - InventoryResponse response = inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId()); - - // then - assertThat(response.displayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); - assertThat(response.warehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(4)); - - Inventory updated = inventoryRepository.findById(inventory.getId()) - .orElseThrow(); - assertThat(updated.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); - assertThat(updated.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(4)); - } - - @Test - void 재고_이동시_다른_상점의_재고면_접근_거부_예외가_발생한다() { - // given - Store store1 = newStore("상점1"); - Store store2 = newStore("상점2"); - - User user1 = newUser(store1, "user1"); - Vendor vendor2 = newVendor(store2, "발주처2"); - Product product2 = newProduct(store2, vendor2, "상품2", "P002"); - - inventoryService.createInventory(product2); - Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) - .orElseThrow(); - - MoveInventoryRequest moveRequest = new MoveInventoryRequest( - BigDecimal.ONE, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - - // when & then - assertThatThrownBy(() -> inventoryService.moveInventory(inventoryOfStore2.getId(), moveRequest, user1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); - } - - @Test - void 창고에서_매대로_이동시_창고_재고보다_많이_요청하면_예외가_발생한다() { - // given - Store store = newStore("이동상점_예외"); - User user = newUser(store, "moveUser_ex"); - Vendor vendor = newVendor(store, "발주처"); - Product product = newProduct(store, vendor, "이동상품", "P001"); - - inventoryService.createInventory(product); - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - UpdateInventoryRequest initRequest = new UpdateInventoryRequest( - product.getId(), - BigDecimal.ZERO, - BigDecimal.valueOf(5), - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); - - // when - MoveInventoryRequest moveRequest = new MoveInventoryRequest( - BigDecimal.valueOf(10), InventoryMoveDirection.WAREHOUSE_TO_DISPLAY - ); - - // then - assertThatThrownBy(() -> inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.WAREHOUSE_STOCK_NOT_ENOUGH.getMessage()); - - Inventory after = inventoryRepository.findById(inventory.getId()) - .orElseThrow(); - assertThat(after.getDisplayStock()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(after.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(5)); - } - - @Test - void 매대에서_창고로_이동시_매대_재고보다_많이_요청하면_예외가_발생한다() { - // given - Store store = newStore("이동상점_예외2"); - User user = newUser(store, "moveUser_ex2"); - Vendor vendor = newVendor(store, "발주처2"); - Product product = newProduct(store, vendor, "이동상품2", "P002"); - - inventoryService.createInventory(product); - Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) - .orElseThrow(); - - UpdateInventoryRequest initRequest = new UpdateInventoryRequest( - product.getId(), - BigDecimal.valueOf(3), - BigDecimal.ONE, - BigDecimal.ZERO, - BigDecimal.ZERO, - null - ); - inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); - - // when - MoveInventoryRequest moveRequest = new MoveInventoryRequest( - BigDecimal.valueOf(5), - InventoryMoveDirection.DISPLAY_TO_WAREHOUSE - ); - - // then - assertThatThrownBy(() -> inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getMessage()); - - Inventory after = inventoryRepository.findById(inventory.getId()) - .orElseThrow(); - assertThat(after.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); - assertThat(after.getWarehouseStock()).isEqualByComparingTo(BigDecimal.ONE); - } + @Autowired + private InventoryService inventoryService; + @Autowired + private InventoryRepository inventoryRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private VendorRepository vendorRepository; + @Autowired + private ProductRepository productRepository; + + private Store newStore(String name) { + return storeRepository.save( + Store.builder() + .name(name) + .isActivate(true) + .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) + .build()); + } + + private User newUser(Store store, String username) { + return userRepository.save( + User.builder() + .store(store) + .username(username) + .password("encoded-password") + .name("테스트 유저") + .role(UserRole.ADMIN) + .build()); + } + + private Vendor newVendor(Store store, String name) { + return vendorRepository.save( + Vendor.builder() + .store(store) + .name(name) + .channel(VendorChannel.KAKAO) + .contactPoint("010-1111-1111") + .note("비고") + .activated(true) + .build()); + } + + private Product newProduct(Store store, Vendor vendor, String name, String code) { + return productRepository.save( + Product.builder() + .store(store) + .vendor(vendor) + .name(name) + .cafe24Code("C-" + code) + .posCode(code) + .unit(ProductUnit.EA) + .boxWeightG(null) + .unitPerBox(null) + .unitWeightG(null) + .activated(true) + .costPrice(1000) + .retailPrice(1500) + .wholesalePrice(1200) + .build()); + } + + @Test + void 재고_수동_수정에_성공한다() { + // given + Store store = newStore("수동수정상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + Product product = newProduct(store, vendor, "상품1", "P001"); + + inventoryService.createInventory(product); + + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + BigDecimal newDisplay = BigDecimal.valueOf(1.234); + BigDecimal newWarehouse = BigDecimal.valueOf(10.000); + BigDecimal newOutgoing = BigDecimal.valueOf(0.500); + BigDecimal newIncoming = BigDecimal.valueOf(3.000); + BigDecimal newReorderTrigger = BigDecimal.valueOf(0.25); + + UpdateInventoryRequest request = new UpdateInventoryRequest( + product.getId(), + newDisplay, + newWarehouse, + newOutgoing, + newIncoming, + newReorderTrigger); + + // when + InventoryResponse response = inventoryService.updateInventory(inventory.getId(), request, user.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.inventoryId()).isEqualTo(inventory.getId()); + assertThat(response.productId()).isEqualTo(product.getId()); + assertThat(response.productName()).isEqualTo(product.getName()); + assertThat(response.cafe24Code()).isEqualTo(product.getCafe24Code()); + assertThat(response.posCode()).isEqualTo(product.getPosCode()); + assertThat(response.displayStock()).isEqualByComparingTo(newDisplay); + assertThat(response.warehouseStock()).isEqualByComparingTo(newWarehouse); + assertThat(response.outgoingReserved()).isEqualByComparingTo(newOutgoing); + assertThat(response.incomingReserved()).isEqualByComparingTo(newIncoming); + assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(newReorderTrigger); + + Inventory updated = inventoryRepository.findById(inventory.getId()) + .orElseThrow(); + assertThat(updated.getDisplayStock()).isEqualByComparingTo(newDisplay); + assertThat(updated.getWarehouseStock()).isEqualByComparingTo(newWarehouse); + assertThat(updated.getOutgoingReserved()).isEqualByComparingTo(newOutgoing); + assertThat(updated.getIncomingReserved()).isEqualByComparingTo(newIncoming); + assertThat(updated.getReorderTriggerPoint()).isEqualByComparingTo(newReorderTrigger); + } + + @Test + void 재고_수동_수정시_품목_아이디가_다르면_예외가_발생한다() { + // given + Store store = newStore("상품불일치상점"); + User user = newUser(store, "mismatchUser"); + Vendor vendor = newVendor(store, "발주처"); + + Product product1 = newProduct(store, vendor, "상품1", "P001"); + Product product2 = newProduct(store, vendor, "상품2", "P002"); + + inventoryService.createInventory(product1); + Inventory inventory = inventoryRepository.findByProduct_Id(product1.getId()) + .orElseThrow(); + + UpdateInventoryRequest request = new UpdateInventoryRequest( + product2.getId(), + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + + // when & then + assertThatThrownBy(() -> inventoryService.updateInventory(inventory.getId(), request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_PRODUCT_MISMATCH.getMessage()); + } + + @Test + void 재고_수동_수정시_다른_상점의_재고면_접근_거부_예외가_발생한다() { + // given + Store store1 = newNewStore("상점1"); + Store store2 = newNewStore("상점2"); + + User user1 = newUser(store1, "user1"); + Vendor vendor2 = newVendor(store2, "발주처2"); + Product product2 = newProduct(store2, vendor2, "상품2", "P002"); + + inventoryService.createInventory(product2); + Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) + .orElseThrow(); + + UpdateInventoryRequest request = new UpdateInventoryRequest( + product2.getId(), + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + + // when & then + assertThatThrownBy(() -> inventoryService.updateInventory(inventoryOfStore2.getId(), request, + user1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); + } + + @Test + void 재고_수동_수정시_사용자가_존재하지_않으면_예외가_발생한다() { + // given + Long notExistUserId = 9999L; + Long anyInventoryId = 1L; + + UpdateInventoryRequest request = new UpdateInventoryRequest( + null, + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + + // when & then + assertThatThrownBy(() -> inventoryService.updateInventory(anyInventoryId, request, notExistUserId)) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 재고_수동_수정시_재고가_존재하지_않으면_예외가_발생한다() { + // given + Store store = newStore("재고없음상점"); + User user = newUser(store, "noInventoryUser"); + Long notExistInventoryId = 9999L; + + UpdateInventoryRequest request = new UpdateInventoryRequest( + null, + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + + // when & then + assertThatThrownBy(() -> inventoryService.updateInventory(notExistInventoryId, request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_NOT_FOUND.getMessage()); + } + + @Test + void 재고_ID로_재고_조회에_성공한다() { + // given + Store store = newStore("조회상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + Product product = newProduct(store, vendor, "상품1", "P001"); + + inventoryService.createInventory(product); + + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + // when + InventoryResponse response = inventoryService.getInventory(inventory.getId(), user.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.inventoryId()).isEqualTo(inventory.getId()); + assertThat(response.productId()).isEqualTo(product.getId()); + assertThat(response.productName()).isEqualTo(product.getName()); + assertThat(response.cafe24Code()).isEqualTo(product.getCafe24Code()); + assertThat(response.posCode()).isEqualTo(product.getPosCode()); + assertThat(response.displayStock()).isEqualByComparingTo(inventory.getDisplayStock()); + assertThat(response.warehouseStock()).isEqualByComparingTo(inventory.getWarehouseStock()); + assertThat(response.outgoingReserved()).isEqualByComparingTo(inventory.getOutgoingReserved()); + assertThat(response.incomingReserved()).isEqualByComparingTo(inventory.getIncomingReserved()); + assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(inventory.getReorderTriggerPoint()); + + } + + @Test + void 재고_ID로_재고_조회시_다른_상점의_재고면_접근_거부_예외가_발생한다() { + // given + Store store1 = newNewStore("상점1"); + Store store2 = newNewStore("상점2"); + + User user1 = newUser(store1, "user1"); + Vendor vendor2 = newVendor(store2, "발주처2"); + Product product2 = newProduct(store2, vendor2, "상품2", "P002"); + + inventoryService.createInventory(product2); + Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) + .orElseThrow(); + + // when & then + assertThatThrownBy(() -> inventoryService.getInventory(inventoryOfStore2.getId(), user1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); + } + + @Test + void 품목으로_재고_조회에_성공한다() { + // given + Store store = newStore("품목조회상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + Product product = newProduct(store, vendor, "상품1", "P001"); + + inventoryService.createInventory(product); + + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + // when + InventoryResponse response = inventoryService.getInventoryByProduct(product.getId(), user.getId()); + + // then + assertThat(response).isNotNull(); + assertThat(response.inventoryId()).isEqualTo(inventory.getId()); + assertThat(response.productId()).isEqualTo(product.getId()); + assertThat(response.productName()).isEqualTo(product.getName()); + assertThat(response.cafe24Code()).isEqualTo(product.getCafe24Code()); + assertThat(response.posCode()).isEqualTo(product.getPosCode()); + assertThat(response.displayStock()).isEqualByComparingTo(inventory.getDisplayStock()); + assertThat(response.warehouseStock()).isEqualByComparingTo(inventory.getWarehouseStock()); + assertThat(response.outgoingReserved()).isEqualByComparingTo(inventory.getOutgoingReserved()); + assertThat(response.incomingReserved()).isEqualByComparingTo(inventory.getIncomingReserved()); + assertThat(response.reorderTriggerPoint()).isEqualByComparingTo(inventory.getReorderTriggerPoint()); + } + + @Test + void 품목으로_재고_조회시_다른_상점의_재고면_접근_거부_예외가_발생한다() { + // given + Store store1 = newStore("상점1"); + Store store2 = newStore("상점2"); + + User user1 = newUser(store1, "user1"); + Vendor vendor2 = newVendor(store2, "발주처2"); + Product product2 = newProduct(store2, vendor2, "상품2", "P002"); + + inventoryService.createInventory(product2); + + // when & then + assertThatThrownBy(() -> inventoryService.getInventoryByProduct(product2.getId(), user1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); + } + + @Test + void 상점_재고_전체_조회에_성공한다() { + // given + Store store = newStore("재고목록상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + + Product product1 = newProduct(store, vendor, "상품A", "P001"); + Product product2 = newProduct(store, vendor, "상품B", "P002"); + Product product3 = newProduct(store, vendor, "상품C", "P003"); + + inventoryService.createInventory(product1); + inventoryService.createInventory(product2); + inventoryService.createInventory(product3); + + Store otherStore = newStore("다른상점"); + Vendor otherVendor = newVendor(otherStore, "다른발주처"); + Product otherProduct = newProduct(otherStore, otherVendor, "다른상품", "PX01"); + inventoryService.createInventory(otherProduct); + + // when + PageResponse pageResponse = inventoryService.getStoreInventoryList(user.getId(), 0, + 20, "all", null, null); + + // then + assertThat(pageResponse).isNotNull(); + assertThat(pageResponse.content()).hasSize(3); + + assertThat(pageResponse.content()) + .extracting(InventoryResponse::productId) + .containsExactlyInAnyOrder( + product1.getId(), + product2.getId(), + product3.getId()); + + assertThat(pageResponse.content()) + .extracting(InventoryResponse::productName) + .containsExactlyInAnyOrder("상품A", "상품B", "상품C"); + } + + @Test + void 상점_재고_목록_조회시_검색어로_필터링된다() { + // given + Store store = newStore("검색상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + + Product target = newProduct(store, vendor, "고체치약 60ea", "P001"); + Product other1 = newProduct(store, vendor, "고무장갑 M", "P002"); + Product other2 = newProduct(store, vendor, "실리콘 용기 540ml", "P003"); + + inventoryService.createInventory(target); + inventoryService.createInventory(other1); + inventoryService.createInventory(other2); + + // when + PageResponse pageResponse = inventoryService.getStoreInventoryList(user.getId(), 0, + 20, "all", "치약", null); + + // then + assertThat(pageResponse).isNotNull(); + assertThat(pageResponse.content()).hasSize(1); + + InventoryResponse result = pageResponse.content().get(0); + assertThat(result.productId()).isEqualTo(target.getId()); + assertThat(result.productName()).isEqualTo("고체치약 60ea"); + } + + @Test + void 상점_재고_목록_조회시_scope가_적용된다() { + // given + Store store = newStore("스코프상점"); + User user = newUser(store, "inventoryUser"); + Vendor vendor = newVendor(store, "발주처"); + + Product displayProduct = newProduct(store, vendor, "매대상품", "P001"); + Product warehouseProduct = newProduct(store, vendor, "창고상품", "P002"); + + inventoryService.createInventory(displayProduct); + inventoryService.createInventory(warehouseProduct); + + Inventory displayInventory = inventoryRepository.findByProduct_Id(displayProduct.getId()) + .orElseThrow(); + Inventory warehouseInventory = inventoryRepository.findByProduct_Id(warehouseProduct.getId()) + .orElseThrow(); + + // 매대상품: display_stock > 0 + UpdateInventoryRequest displayUpdate = new UpdateInventoryRequest( + displayProduct.getId(), + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(displayInventory.getId(), displayUpdate, user.getId()); + + // 창고상품: warehouse_stock > 0, display_stock = 0 + UpdateInventoryRequest warehouseUpdate = new UpdateInventoryRequest( + warehouseProduct.getId(), + BigDecimal.ZERO, + BigDecimal.TEN, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(warehouseInventory.getId(), warehouseUpdate, user.getId()); + + // when: scope = display + PageResponse pageResponse = inventoryService.getStoreInventoryList(user.getId(), 0, + 20, "display", null, null); + + // then + assertThat(pageResponse).isNotNull(); + assertThat(pageResponse.content()).hasSize(1); + + InventoryResponse only = pageResponse.content().get(0); + assertThat(only.productId()).isEqualTo(displayProduct.getId()); + assertThat(only.productName()).isEqualTo("매대상품"); + } + + @Test + void 상점_재고_목록_조회시_상품명_기준_오름차순_정렬이_적용된다() { + // given + Store store = newStore("정렬상점"); + User user = newUser(store, "sortUser"); + Vendor vendor = newVendor(store, "발주처"); + + Product p3 = newProduct(store, vendor, "치약", "P003"); + Product p1 = newProduct(store, vendor, "고무장갑", "P001"); + Product p2 = newProduct(store, vendor, "실리콘 용기", "P002"); + + inventoryService.createInventory(p3); + inventoryService.createInventory(p1); + inventoryService.createInventory(p2); + + // when: sort=productName + PageResponse response = inventoryService.getStoreInventoryList(user.getId(), 0, 20, + "all", null, "productName"); + + // then + assertThat(response).isNotNull(); + assertThat(response.content()).hasSize(3); + + // 오름차순 정렬 결과 체크 + assertThat(response.content()) + .extracting(InventoryResponse::productName) + .containsExactly("고무장갑", "실리콘 용기", "치약"); + } + + @Test + void 창고에서_매대로_재고_이동에_성공한다() { + // given + Store store = newStore("이동상점"); + User user = newUser(store, "moveUser"); + Vendor vendor = newVendor(store, "발주처"); + Product product = newProduct(store, vendor, "이동상품", "P001"); + + inventoryService.createInventory(product); + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + UpdateInventoryRequest initRequest = new UpdateInventoryRequest( + product.getId(), + BigDecimal.ZERO, + BigDecimal.TEN, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); + + // when + MoveInventoryRequest moveRequest = new MoveInventoryRequest( + BigDecimal.valueOf(3), + InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + InventoryResponse response = inventoryService.moveInventory(inventory.getId(), moveRequest, + user.getId()); + + // then + assertThat(response.displayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(response.warehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(7)); + + Inventory updated = inventoryRepository.findById(inventory.getId()) + .orElseThrow(); + assertThat(updated.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(updated.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(7)); + } + + @Test + void 매대에서_창고로_재고_이동에_성공한다() { + // given + Store store = newStore("이동상점2"); + User user = newUser(store, "moveUser2"); + Vendor vendor = newVendor(store, "발주처2"); + Product product = newProduct(store, vendor, "이동상품2", "P002"); + + inventoryService.createInventory(product); + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + UpdateInventoryRequest initRequest = new UpdateInventoryRequest( + product.getId(), + BigDecimal.valueOf(5), + BigDecimal.valueOf(2), + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); + + // when + MoveInventoryRequest moveRequest = new MoveInventoryRequest( + BigDecimal.valueOf(2), + InventoryMoveDirection.DISPLAY_TO_WAREHOUSE); + InventoryResponse response = inventoryService.moveInventory(inventory.getId(), moveRequest, + user.getId()); + + // then + assertThat(response.displayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(response.warehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(4)); + + Inventory updated = inventoryRepository.findById(inventory.getId()) + .orElseThrow(); + assertThat(updated.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(updated.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(4)); + } + + @Test + void 재고_이동시_다른_상점의_재고면_접근_거부_예외가_발생한다() { + // given + Store store1 = newStore("상점1"); + Store store2 = newStore("상점2"); + + User user1 = newUser(store1, "user1"); + Vendor vendor2 = newVendor(store2, "발주처2"); + Product product2 = newProduct(store2, vendor2, "상품2", "P002"); + + inventoryService.createInventory(product2); + Inventory inventoryOfStore2 = inventoryRepository.findByProduct_Id(product2.getId()) + .orElseThrow(); + + MoveInventoryRequest moveRequest = new MoveInventoryRequest( + BigDecimal.ONE, InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + + // when & then + assertThatThrownBy(() -> inventoryService.moveInventory(inventoryOfStore2.getId(), moveRequest, + user1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.INVENTORY_ACCESS_DENIED.getMessage()); + } + + @Test + void 창고에서_매대로_이동시_창고_재고보다_많이_요청하면_예외가_발생한다() { + // given + Store store = newStore("이동상점_예외"); + User user = newUser(store, "moveUser_ex"); + Vendor vendor = newVendor(store, "발주처"); + Product product = newProduct(store, vendor, "이동상품", "P001"); + + inventoryService.createInventory(product); + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + UpdateInventoryRequest initRequest = new UpdateInventoryRequest( + product.getId(), + BigDecimal.ZERO, + BigDecimal.valueOf(5), + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); + + // when + MoveInventoryRequest moveRequest = new MoveInventoryRequest( + BigDecimal.valueOf(10), InventoryMoveDirection.WAREHOUSE_TO_DISPLAY); + + // then + assertThatThrownBy(() -> inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.WAREHOUSE_STOCK_NOT_ENOUGH.getMessage()); + + Inventory after = inventoryRepository.findById(inventory.getId()) + .orElseThrow(); + assertThat(after.getDisplayStock()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(after.getWarehouseStock()).isEqualByComparingTo(BigDecimal.valueOf(5)); + } + + @Test + void 매대에서_창고로_이동시_매대_재고보다_많이_요청하면_예외가_발생한다() { + // given + Store store = newStore("이동상점_예외2"); + User user = newUser(store, "moveUser_ex2"); + Vendor vendor = newVendor(store, "발주처2"); + Product product = newProduct(store, vendor, "이동상품2", "P002"); + + inventoryService.createInventory(product); + Inventory inventory = inventoryRepository.findByProduct_Id(product.getId()) + .orElseThrow(); + + UpdateInventoryRequest initRequest = new UpdateInventoryRequest( + product.getId(), + BigDecimal.valueOf(3), + BigDecimal.ONE, + BigDecimal.ZERO, + BigDecimal.ZERO, + null); + inventoryService.updateInventory(inventory.getId(), initRequest, user.getId()); + + // when + MoveInventoryRequest moveRequest = new MoveInventoryRequest( + BigDecimal.valueOf(5), + InventoryMoveDirection.DISPLAY_TO_WAREHOUSE); + + // then + assertThatThrownBy(() -> inventoryService.moveInventory(inventory.getId(), moveRequest, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.DISPLAY_STOCK_NOT_ENOUGH.getMessage()); + + Inventory after = inventoryRepository.findById(inventory.getId()) + .orElseThrow(); + assertThat(after.getDisplayStock()).isEqualByComparingTo(BigDecimal.valueOf(3)); + assertThat(after.getWarehouseStock()).isEqualByComparingTo(BigDecimal.ONE); + } } diff --git a/src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java b/src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java index bcedc8a7..597a96b2 100644 --- a/src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java +++ b/src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java @@ -42,594 +42,603 @@ @ActiveProfiles("test") public class ProductControllerTest { - @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - - @MockitoBean private ProductService productService; - @MockitoBean private JpaMetamodelMappingContext jpaMetamodelMappingContext; - @MockitoBean private DiscordErrorNotifier discordErrorNotifier; - - private UsernamePasswordAuthenticationToken auth() { - CustomUserPrincipal principal = - new CustomUserPrincipal(1L, "store_admin", List.of()); - return new UsernamePasswordAuthenticationToken( - principal, null, principal.getAuthorities() - ); - } - - @Test - void 품목_등록에_성공한다() throws Exception { - // given - CreateProductRequest request = new CreateProductRequest( - 1L, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - ProductResponse response = new ProductResponse( - 1L, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - true, - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200, - 1L, - 1L - ); - - when(productService.createProduct(any(CreateProductRequest.class), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(post("/api/v1/product") - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.CREATE_PRODUCT_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.name").value("고체치약")) - .andExpect(jsonPath("$.data.code").value("P-001")) - .andExpect(jsonPath("$.data.unit").value(ProductUnit.G.name())) - .andExpect(jsonPath("$.data.isActivated").value(true)) - .andExpect(jsonPath("$.data.storeId").value(1L)) - .andExpect(jsonPath("$.data.vendorId").value(1L)); - } - - @Test - void 품목_등록_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - CreateProductRequest request = new CreateProductRequest( - 1L, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - when(productService.createProduct(any(CreateProductRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(post("/api/v1/product") - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_등록_시_발주처가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - CreateProductRequest request = new CreateProductRequest( - 9999L, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - when(productService.createProduct(any(CreateProductRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.VENDOR_NOT_FOUND)); - - // when & then - mockMvc.perform(post("/api/v1/product") - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.VENDOR_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.VENDOR_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_등록_요청값_검증에_실패하면_예외가_발생한다() throws Exception { - // given - CreateProductRequest invalidRequest = new CreateProductRequest( - null, - "", - "", - null, - null, - 0, - null, - 0, - 0, - 0 - ); - - // when & then - mockMvc.perform(post("/api/v1/product") - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_수정에_성공한다() throws Exception { - // given - Long productId = 1L; - - UpdateProductRequest request = new UpdateProductRequest( - 1L, - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - 20, - BigDecimal.valueOf(110.0), - false, - 2000, - 2500, - 2200 - ); - - ProductResponse response = new ProductResponse( - productId, - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - false, - 20, - BigDecimal.valueOf(110.0), - 2000, - 2500, - 2200, - 1L, - 1L - ); - - when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(patch("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.UPDATE_PRODUCT_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.name").value("수정된 고체치약")) - .andExpect(jsonPath("$.data.code").value("P-999")) - .andExpect(jsonPath("$.data.unit").value(ProductUnit.ML.name())) - .andExpect(jsonPath("$.data.isActivated").value(false)) - .andExpect(jsonPath("$.data.storeId").value(1L)) - .andExpect(jsonPath("$.data.vendorId").value(1L)); - } - - @Test - void 품목_수정_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long productId = 9999L; - - UpdateProductRequest request = new UpdateProductRequest( - 1L, - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - 20, - BigDecimal.valueOf(110.0), - false, - 2000, - 2500, - 2200 - ); - - when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); - - // when & then - mockMvc.perform(patch("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_수정_시_발주처가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long productId = 1L; - - UpdateProductRequest request = new UpdateProductRequest( - 9999L, - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - 20, - BigDecimal.valueOf(110.0), - false, - 2000, - 2500, - 2200 - ); - - when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.VENDOR_NOT_FOUND)); - - // when & then - mockMvc.perform(patch("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.VENDOR_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.VENDOR_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_수정_시_다른_상점_발주처이면_예외가_발생한다() throws Exception { - // given - Long productId = 1L; - - UpdateProductRequest request = new UpdateProductRequest( - 2L, - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - 20, - BigDecimal.valueOf(110.0), - false, - 2000, - 2500, - 2200 - ); - - when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) - .thenThrow(new BaseException(ErrorCode.VENDOR_ACCESS_DENIED)); - - // when & then - mockMvc.perform(patch("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.VENDOR_ACCESS_DENIED.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.VENDOR_ACCESS_DENIED.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_수정_요청값_검증에_실패하면_예외가_발생한다() throws Exception { - // given - UpdateProductRequest invalidRequest = new UpdateProductRequest( - null, - "", - "", - null, - null, - 0, - null, - true, - -1, - -1, - -1 - ); - - // when & then - mockMvc.perform(patch("/api/v1/product/{productId}", 1L) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(invalidRequest))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_상세_조회에_성공한다() throws Exception { - // given - Long productId = 1L; - - ProductResponse response = new ProductResponse( - productId, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - true, - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200, - 1L, - 1L - ); - - when(productService.getProductDetail(anyLong(), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(get("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.GET_PRODUCT_DETAIL_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.productId").value(1L)) - .andExpect(jsonPath("$.data.name").value("고체치약")) - .andExpect(jsonPath("$.data.code").value("P-001")) - .andExpect(jsonPath("$.data.unit").value(ProductUnit.G.name())) - .andExpect(jsonPath("$.data.isActivated").value(true)) - .andExpect(jsonPath("$.data.storeId").value(1L)) - .andExpect(jsonPath("$.data.vendorId").value(1L)); - } - - @Test - void 품목_상세_조회_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long notExistProductId = 9999L; - - when(productService.getProductDetail(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/product/{productId}", notExistProductId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_상세_조회_시_다른_상점_품목이면_예외가_발생한다() throws Exception { - // given - Long productId = 1L; - - when(productService.getProductDetail(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.STORE_ACCESS_DENIED)); - - // when & then - mockMvc.perform(get("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.STORE_ACCESS_DENIED.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.STORE_ACCESS_DENIED.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_목록_조회에_성공한다() throws Exception { - // given - ProductResponse product1 = new ProductResponse( - 1L, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - true, - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200, - 1L, - 1L - ); - - ProductResponse product2 = new ProductResponse( - 2L, - "고무장갑", - "P-002", - ProductUnit.G, - BigDecimal.valueOf(800.0), - true, - 20, - BigDecimal.valueOf(40.0), - 500, - 1000, - 800, - 1L, - 2L - ); - - PageResponse pageResponse = new PageResponse<>( - List.of(product1, product2), - 1, - 10, - 2L, - 1, - true - ); - - when(productService.getProductList(anyLong(), any(), any(), any(), any())) - .thenReturn(pageResponse); - - // when & then - mockMvc.perform(get("/api/v1/product") - .with(authentication(auth())) - .param("page", "1") - .param("size", "10") - .param("isActivate", "true") - .param("name", "고")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message") - .value(SuccessMessage.GET_PRODUCT_LIST_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.page").value(1)) - .andExpect(jsonPath("$.data.size").value(10)) - .andExpect(jsonPath("$.data.totalElements").value(2)) - .andExpect(jsonPath("$.data.totalPages").value(1)) - .andExpect(jsonPath("$.data.last").value(true)) - .andExpect(jsonPath("$.data.content[0].productId").value(1L)) - .andExpect(jsonPath("$.data.content[0].name").value("고체치약")) - .andExpect(jsonPath("$.data.content[0].code").value("P-001")) - .andExpect(jsonPath("$.data.content[0].unit").value(ProductUnit.G.name())) - .andExpect(jsonPath("$.data.content[0].isActivated").value(true)) - .andExpect(jsonPath("$.data.content[0].storeId").value(1L)) - .andExpect(jsonPath("$.data.content[0].vendorId").value(1L)); - } - - @Test - void 품목_목록_조회_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - when(productService.getProductList(anyLong(), any(), any(), any(), any())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(get("/api/v1/product") - .with(authentication(auth())) - .param("page", "1") - .param("size", "10")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status") - .value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message") - .value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_삭제에_성공한다() throws Exception { - // given - Long productId = 1L; - DeleteProductResponse response = new DeleteProductResponse(true); - - when(productService.deleteProduct(anyLong(), anyLong())) - .thenReturn(response); - - // when & then - mockMvc.perform(delete("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(200)) - .andExpect(jsonPath("$.message").value(SuccessMessage.DELETE_PRODUCT_SUCCESS.getMessage())) - .andExpect(jsonPath("$.data.success").value(true)); - } - - @Test - void 품목_삭제_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long notExistProductId = 9999L; - - when(productService.deleteProduct(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); - - // when & then - mockMvc.perform(delete("/api/v1/product/{productId}", notExistProductId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_삭제_시_다른_상점_품목이면_예외가_발생한다() throws Exception { - // given - Long productId = 1L; - - when(productService.deleteProduct(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.STORE_ACCESS_DENIED)); - - // when & then - mockMvc.perform(delete("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.status").value(ErrorCode.STORE_ACCESS_DENIED.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.STORE_ACCESS_DENIED.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } - - @Test - void 품목_삭제_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { - // given - Long productId = 1L; - - when(productService.deleteProduct(anyLong(), anyLong())) - .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); - - // when & then - mockMvc.perform(delete("/api/v1/product/{productId}", productId) - .with(authentication(auth())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) - .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) - .andExpect(jsonPath("$.data").doesNotExist()); - } + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + + @MockitoBean + private ProductService productService; + @MockitoBean + private JpaMetamodelMappingContext jpaMetamodelMappingContext; + @MockitoBean + private DiscordErrorNotifier discordErrorNotifier; + + private UsernamePasswordAuthenticationToken auth() { + CustomUserPrincipal principal = new CustomUserPrincipal(1L, "store_admin", List.of()); + return new UsernamePasswordAuthenticationToken( + principal, null, principal.getAuthorities()); + } + + @Test + void 품목_등록에_성공한다() throws Exception { + // given + CreateProductRequest request = new CreateProductRequest( + 1L, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + ProductResponse response = new ProductResponse( + 1L, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + true, + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200, + 1L, + 1L); + + when(productService.createProduct(any(CreateProductRequest.class), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(post("/api/v1/product") + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.CREATE_PRODUCT_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.name").value("고체치약")) + .andExpect(jsonPath("$.data.cafe24Code").value("C-001")) + .andExpect(jsonPath("$.data.posCode").value("P-001")) + .andExpect(jsonPath("$.data.unit").value(ProductUnit.G.name())) + .andExpect(jsonPath("$.data.isActivated").value(true)) + .andExpect(jsonPath("$.data.storeId").value(1L)) + .andExpect(jsonPath("$.data.vendorId").value(1L)); + } + + @Test + void 품목_등록_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + CreateProductRequest request = new CreateProductRequest( + 1L, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + when(productService.createProduct(any(CreateProductRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(post("/api/v1/product") + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_등록_시_발주처가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + CreateProductRequest request = new CreateProductRequest( + 9999L, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + when(productService.createProduct(any(CreateProductRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.VENDOR_NOT_FOUND)); + + // when & then + mockMvc.perform(post("/api/v1/product") + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.VENDOR_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.VENDOR_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_등록_요청값_검증에_실패하면_예외가_발생한다() throws Exception { + // given + CreateProductRequest invalidRequest = new CreateProductRequest( + null, + "", + "", + "", + null, + null, + 0, + null, + 0, + 0, + 0); + + // when & then + mockMvc.perform(post("/api/v1/product") + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_수정에_성공한다() throws Exception { + // given + Long productId = 1L; + + UpdateProductRequest request = new UpdateProductRequest( + 1L, + "수정된 고체치약", + "C-999", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + 20, + BigDecimal.valueOf(110.0), + false, + 2000, + 2500, + 2200); + + ProductResponse response = new ProductResponse( + productId, + "수정된 고체치약", + "C-999", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + false, + 20, + BigDecimal.valueOf(110.0), + 2000, + 2500, + 2200, + 1L, + 1L); + + when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(patch("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.UPDATE_PRODUCT_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.name").value("수정된 고체치약")) + .andExpect(jsonPath("$.data.cafe24Code").value("C-999")) + .andExpect(jsonPath("$.data.posCode").value("P-999")) + .andExpect(jsonPath("$.data.unit").value(ProductUnit.ML.name())) + .andExpect(jsonPath("$.data.isActivated").value(false)) + .andExpect(jsonPath("$.data.storeId").value(1L)) + .andExpect(jsonPath("$.data.vendorId").value(1L)); + } + + @Test + void 품목_수정_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long productId = 9999L; + + UpdateProductRequest request = new UpdateProductRequest( + 1L, + "수정된 고체치약", + "C-999", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + 20, + BigDecimal.valueOf(110.0), + false, + 2000, + 2500, + 2200); + + when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); + + // when & then + mockMvc.perform(patch("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_수정_시_발주처가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long productId = 1L; + + UpdateProductRequest request = new UpdateProductRequest( + 9999L, + "수정된 고체치약", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + 20, + BigDecimal.valueOf(110.0), + false, + 2000, + 2500, + 2200); + + when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.VENDOR_NOT_FOUND)); + + // when & then + mockMvc.perform(patch("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.VENDOR_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.VENDOR_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_수정_시_다른_상점_발주처이면_예외가_발생한다() throws Exception { + // given + Long productId = 1L; + + UpdateProductRequest request = new UpdateProductRequest( + 2L, + "수정된 고체치약", + "C-999", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + 20, + BigDecimal.valueOf(110.0), + false, + 2000, + 2500, + 2200); + + when(productService.updateProduct(anyLong(), any(UpdateProductRequest.class), anyLong())) + .thenThrow(new BaseException(ErrorCode.VENDOR_ACCESS_DENIED)); + + // when & then + mockMvc.perform(patch("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.VENDOR_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.VENDOR_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_수정_요청값_검증에_실패하면_예외가_발생한다() throws Exception { + // given + UpdateProductRequest invalidRequest = new UpdateProductRequest( + null, + "", + "", + "", + null, + null, + 0, + null, + true, + -1, + -1, + -1); + + // when & then + mockMvc.perform(patch("/api/v1/product/{productId}", 1L) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.INVALID_INPUT_VALUE.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_상세_조회에_성공한다() throws Exception { + // given + Long productId = 1L; + + ProductResponse response = new ProductResponse( + productId, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + true, + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200, + 1L, + 1L); + + when(productService.getProductDetail(anyLong(), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(get("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_PRODUCT_DETAIL_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.productId").value(1L)) + .andExpect(jsonPath("$.data.name").value("고체치약")) + .andExpect(jsonPath("$.data.cafe24Code").value("C-001")) + .andExpect(jsonPath("$.data.posCode").value("P-001")) + .andExpect(jsonPath("$.data.unit").value(ProductUnit.G.name())) + .andExpect(jsonPath("$.data.isActivated").value(true)) + .andExpect(jsonPath("$.data.storeId").value(1L)) + .andExpect(jsonPath("$.data.vendorId").value(1L)); + } + + @Test + void 품목_상세_조회_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long notExistProductId = 9999L; + + when(productService.getProductDetail(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/product/{productId}", notExistProductId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_상세_조회_시_다른_상점_품목이면_예외가_발생한다() throws Exception { + // given + Long productId = 1L; + + when(productService.getProductDetail(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.STORE_ACCESS_DENIED)); + + // when & then + mockMvc.perform(get("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.STORE_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.STORE_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_목록_조회에_성공한다() throws Exception { + // given + ProductResponse product1 = new ProductResponse( + 1L, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + true, + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200, + 1L, + 1L); + + ProductResponse product2 = new ProductResponse( + 2L, + "고무장갑", + "C-002", + "P-002", + ProductUnit.G, + BigDecimal.valueOf(800.0), + true, + 20, + BigDecimal.valueOf(40.0), + 500, + 1000, + 800, + 1L, + 2L); + + PageResponse pageResponse = new PageResponse<>( + List.of(product1, product2), + 1, + 10, + 2L, + 1, + true); + + when(productService.getProductList(anyLong(), any(), any(), any(), any())) + .thenReturn(pageResponse); + + // when & then + mockMvc.perform(get("/api/v1/product") + .with(authentication(auth())) + .param("page", "1") + .param("size", "10") + .param("isActivate", "true") + .param("name", "고")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_PRODUCT_LIST_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.page").value(1)) + .andExpect(jsonPath("$.data.size").value(10)) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.last").value(true)) + .andExpect(jsonPath("$.data.content[0].productId").value(1L)) + .andExpect(jsonPath("$.data.content[0].name").value("고체치약")) + .andExpect(jsonPath("$.data.content[0].cafe24Code").value("C-001")) + .andExpect(jsonPath("$.data.content[0].posCode").value("P-001")) + .andExpect(jsonPath("$.data.content[0].unit").value(ProductUnit.G.name())) + .andExpect(jsonPath("$.data.content[0].isActivated").value(true)) + .andExpect(jsonPath("$.data.content[0].storeId").value(1L)) + .andExpect(jsonPath("$.data.content[0].vendorId").value(1L)); + } + + @Test + void 품목_목록_조회_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + when(productService.getProductList(anyLong(), any(), any(), any(), any())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/product") + .with(authentication(auth())) + .param("page", "1") + .param("size", "10")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_삭제에_성공한다() throws Exception { + // given + Long productId = 1L; + DeleteProductResponse response = new DeleteProductResponse(true); + + when(productService.deleteProduct(anyLong(), anyLong())) + .thenReturn(response); + + // when & then + mockMvc.perform(delete("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.DELETE_PRODUCT_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.success").value(true)); + } + + @Test + void 품목_삭제_시_품목이_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long notExistProductId = 9999L; + + when(productService.deleteProduct(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.PRODUCT_NOT_FOUND)); + + // when & then + mockMvc.perform(delete("/api/v1/product/{productId}", notExistProductId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.PRODUCT_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.PRODUCT_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_삭제_시_다른_상점_품목이면_예외가_발생한다() throws Exception { + // given + Long productId = 1L; + + when(productService.deleteProduct(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.STORE_ACCESS_DENIED)); + + // when & then + mockMvc.perform(delete("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.STORE_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.STORE_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 품목_삭제_시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + Long productId = 1L; + + when(productService.deleteProduct(anyLong(), anyLong())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(delete("/api/v1/product/{productId}", productId) + .with(authentication(auth())) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } } diff --git a/src/test/java/com/almang/inventory/product/service/ProductServiceTest.java b/src/test/java/com/almang/inventory/product/service/ProductServiceTest.java index 78ab93d5..b9b69fe7 100644 --- a/src/test/java/com/almang/inventory/product/service/ProductServiceTest.java +++ b/src/test/java/com/almang/inventory/product/service/ProductServiceTest.java @@ -35,1193 +35,1138 @@ @ActiveProfiles("test") public class ProductServiceTest { - @Autowired private ProductService productService; - @Autowired private ProductRepository productRepository; - @Autowired private UserRepository userRepository; - @Autowired private StoreRepository storeRepository; - @Autowired private VendorRepository vendorRepository; - @Autowired private InventoryRepository inventoryRepository; - - private Store newStore() { - return storeRepository.save( - Store.builder() - .name("테스트 상점") - .isActivate(true) - .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) - .build() - ); - } - - private Vendor newVendor(Store store) { - return vendorRepository.save( - Vendor.builder() - .store(store) - .name("테스트 발주처") - .channel(VendorChannel.KAKAO) - .contactPoint("010-0000-0000") - .note("테스트 메모") - .activated(true) - .build() - ); - } - - private User newUser(Store store) { - return userRepository.save( - User.builder() - .store(store) - .username("tester") - .password("password") - .name("테스트 유저") - .role(UserRole.ADMIN) - .build() - ); - } - - @Test - void 품목_생성에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - CreateProductRequest request = new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - // when - ProductResponse response = productService.createProduct(request, user.getId()); - - // then - assertThat(response.name()).isEqualTo("고체치약"); - assertThat(response.code()).isEqualTo("P-001"); - assertThat(response.unit()).isEqualTo(ProductUnit.G); - assertThat(response.boxWeightG()).isEqualByComparingTo("1000.0"); - assertThat(response.unitPerBox()).isEqualTo(10); - assertThat(response.unitWeightG()).isEqualByComparingTo("100.0"); - assertThat(response.costPrice()).isEqualTo(1000); - assertThat(response.retailPrice()).isEqualTo(1500); - assertThat(response.wholesalePrice()).isEqualTo(1200); - assertThat(response.storeId()).isEqualTo(store.getId()); - assertThat(response.vendorId()).isEqualTo(vendor.getId()); - assertThat(response.isActivated()).isTrue(); - - List inventories = inventoryRepository.findAll(); - assertThat(inventories).hasSize(1); - - Inventory inventory = inventories.get(0); - assertThat(inventory.getProduct().getId()).isEqualTo(response.productId()); - assertThat(inventory.getDisplayStock()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(inventory.getWarehouseStock()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(inventory.getIncomingReserved()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(inventory.getOutgoingReserved()).isEqualByComparingTo(BigDecimal.ZERO); - assertThat(inventory.getReorderTriggerPoint()) - .isEqualByComparingTo(store.getDefaultCountCheckThreshold()); - } - - @Test - void 사용자_존재하지_않으면_품목_생성시_예외가_발생한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - - Long notExistUserId = 9999L; - - CreateProductRequest request = new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - // when & then - assertThatThrownBy(() -> productService.createProduct(request, notExistUserId)) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); - } - - @Test - void 발주처가_존재하지_않으면_품목_생성시_예외가_발생한다() { - // given - Store store = newStore(); - User user = newUser(store); - - Long notExistVendorId = 9999L; - - CreateProductRequest request = new CreateProductRequest( - notExistVendorId, - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - // when & then - assertThatThrownBy(() -> productService.createProduct(request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage()); - } - - @Test - void 다른_상점의_발주처로_품목_생성시_예외가_발생한다() { - // given - Store store1 = newStore(); - Store store2 = storeRepository.save( - Store.builder() - .name("다른 상점") - .isActivate(true) - .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) - .build() - ); - - Vendor vendorOfStore2 = newVendor(store2); - User userOfStore1 = newUser(store1); - - CreateProductRequest request = new CreateProductRequest( - vendorOfStore2.getId(), // 다른 상점의 발주처! - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - 1000, - 1500, - 1200 - ); - - // when & then - assertThatThrownBy(() -> productService.createProduct(request, userOfStore1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.VENDOR_ACCESS_DENIED.getMessage()); - } - - @Test - void 품목_수정에_성공한다() { - // given - Store store = newStore(); - Vendor vendor1 = newVendor(store); - Vendor vendor2 = newVendor(store); - User user = newUser(store); - - CreateProductRequest createRequest = new CreateProductRequest( - vendor1.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ); - - ProductResponse created = productService.createProduct(createRequest, user.getId()); - - // when - UpdateProductRequest updateRequest = new UpdateProductRequest( - vendor2.getId(), - "수정된 고체치약", - "P-999", - ProductUnit.ML, - BigDecimal.valueOf(1200.0), - 20, - BigDecimal.valueOf(110.0), - false, - 2000, - 2500, - 2200 - ); - - ProductResponse updated = productService.updateProduct(created.productId(), updateRequest, user.getId()); - - // then - assertThat(updated.name()).isEqualTo("수정된 고체치약"); - assertThat(updated.code()).isEqualTo("P-999"); - assertThat(updated.unit()).isEqualTo(ProductUnit.ML); - assertThat(updated.boxWeightG()).isEqualByComparingTo("1200.0"); - assertThat(updated.unitPerBox()).isEqualTo(20); - assertThat(updated.unitWeightG()).isEqualByComparingTo("110.0"); - assertThat(updated.isActivated()).isFalse(); - assertThat(updated.costPrice()).isEqualTo(2000); - assertThat(updated.retailPrice()).isEqualTo(2500); - assertThat(updated.wholesalePrice()).isEqualTo(2200); - assertThat(updated.vendorId()).isEqualTo(vendor2.getId()); - } - - @Test - void 존재하지_않는_품목을_수정하려고_하면_예외가_발생한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - Long notExistProductId = 9999L; - - UpdateProductRequest request = new UpdateProductRequest( - vendor.getId(), - "변경 이름", - "CODE", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - true, - 1000, - 2000, - 1500 - ); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(notExistProductId, request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); - } - - @Test - void 존재하지_않는_발주처로_수정시_예외가_발생한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - ProductResponse created = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - Long notExistVendorId = 9999L; - - UpdateProductRequest request = new UpdateProductRequest( - notExistVendorId, - "변경됨", - "NEW", - ProductUnit.G, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - true, - 2000, - 3000, - 2500 - ); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(created.productId(), request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage()); - } - - @Test - void 다른_상점의_발주처로_수정하면_예외가_발생한다() { - // given - Store store1 = newStore(); - Store store2 = newStore(); - - Vendor vendor1 = newVendor(store1); - Vendor vendorOfStore2 = newVendor(store2); - User user = newUser(store1); - - ProductResponse created = productService.createProduct( - new CreateProductRequest( - vendor1.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - UpdateProductRequest request = new UpdateProductRequest( - vendorOfStore2.getId(), - "변경됨", - "NEW", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - true, - 2000, - 3000, - 2500 - ); - - // when & then - assertThatThrownBy(() -> productService.updateProduct(created.productId(), request, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.VENDOR_ACCESS_DENIED.getMessage()); - } - - @Test - void 다른_상점의_품목을_수정하려고_하면_예외가_발생한다() { - // given - Store store1 = newStore(); - Store store2 = newStore(); - - Vendor vendorOfStore1 = newVendor(store1); - Vendor vendorOfStore2 = newVendor(store2); - - User userOfStore1 = newUser(store1); - User userOfStore2 = userRepository.save( - User.builder() - .store(store2) - .username("tester_store2") - .password("password") - .name("상점2 관리자") - .role(UserRole.ADMIN) - .build() - ); - - ProductResponse productOfStore2 = productService.createProduct( - new CreateProductRequest( - vendorOfStore2.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - userOfStore2.getId() - ); - - // when & then - UpdateProductRequest request = new UpdateProductRequest( - vendorOfStore1.getId(), - "변경됨", - "NEW", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 10, - BigDecimal.valueOf(100.0), - true, - 2000, - 3000, - 2500 - ); - - assertThatThrownBy(() -> - productService.updateProduct(productOfStore2.productId(), request, userOfStore1.getId()) - ) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); - } - - @Test - void 품목_상세_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - ProductResponse created = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - // when - ProductResponse detail = productService.getProductDetail(created.productId(), user.getId()); - - // then - assertThat(detail.productId()).isEqualTo(created.productId()); - assertThat(detail.name()).isEqualTo("고체치약"); - assertThat(detail.code()).isEqualTo("P-001"); - assertThat(detail.unit()).isEqualTo(ProductUnit.G); - assertThat(detail.boxWeightG()).isEqualByComparingTo("900.0"); - assertThat(detail.unitPerBox()).isEqualTo(10); - assertThat(detail.unitWeightG()).isEqualByComparingTo("90.0"); - assertThat(detail.costPrice()).isEqualTo(1000); - assertThat(detail.retailPrice()).isEqualTo(1500); - assertThat(detail.wholesalePrice()).isEqualTo(1200); - assertThat(detail.storeId()).isEqualTo(store.getId()); - assertThat(detail.vendorId()).isEqualTo(vendor.getId()); - assertThat(detail.isActivated()).isTrue(); - } - - @Test - void 존재하지_않는_품목_상세_조회시_예외가_발생한다() { - // given - Store store = newStore(); - User user = newUser(store); - Long notExistProductId = 9999L; - - // when & then - assertThatThrownBy(() -> productService.getProductDetail(notExistProductId, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); - } - - @Test - void 다른_상점의_품목을_상세_조회하면_예외가_발생한다() { - // given - Store store1 = newStore(); - Store store2 = newStore(); - - Vendor vendorOfStore2 = newVendor(store2); - - User userOfStore1 = newUser(store1); - User userOfStore2 = userRepository.save( - User.builder() - .store(store2) - .username("detail_tester_store2") - .password("password") - .name("상점2 관리자(상세조회)") - .role(UserRole.ADMIN) - .build() - ); - - ProductResponse productOfStore2 = productService.createProduct( - new CreateProductRequest( - vendorOfStore2.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - userOfStore2.getId() - ); - - // when & then - assertThatThrownBy(() -> - productService.getProductDetail(productOfStore2.productId(), userOfStore1.getId()) - ) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); - } - - @Test - void 품목_목록_전체_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "세제", - "P-003", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 5, - BigDecimal.valueOf(200.0), - 3000, - 4000, - 3500 - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, null, null); - - // then - assertThat(page.page()).isEqualTo(1); - assertThat(page.size()).isEqualTo(10); - assertThat(page.totalElements()).isEqualTo(3); - assertThat(page.content()).hasSize(3); - } - - @Test - void 품목_목록_활성_필터_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - // 활성 품목 - ProductResponse active = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - // 비활성으로 바꿀 품목 - ProductResponse willBeInactive = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - - // 고무장갑 비활성 처리 - productService.updateProduct( - willBeInactive.productId(), - new UpdateProductRequest( - vendor.getId(), - willBeInactive.name(), - willBeInactive.code(), - willBeInactive.unit(), - willBeInactive.boxWeightG(), - willBeInactive.unitPerBox(), - willBeInactive.unitWeightG(), - false, - willBeInactive.costPrice(), - willBeInactive.retailPrice(), - willBeInactive.wholesalePrice() - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, true, null); - - // then - assertThat(page.totalElements()).isEqualTo(1); - assertThat(page.content()).hasSize(1); - assertThat(page.content().get(0).productId()).isEqualTo(active.productId()); - assertThat(page.content().get(0).isActivated()).isTrue(); - } - - @Test - void 품목_목록_이름검색으로_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "세제", - "P-003", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 5, - BigDecimal.valueOf(200.0), - 3000, - 4000, - 3500 - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, null, "고"); - - // then - assertThat(page.totalElements()).isEqualTo(2); - assertThat(page.content()).hasSize(2); - assertThat(page.content()) - .extracting(ProductResponse::name) - .allMatch(name -> name.contains("고")); - } - - @Test - void 품목_목록_활성_및_이름검색_동시_필터링_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - ProductResponse activeMatch = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - ProductResponse inactiveMatch = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - - // 고무장갑 비활성 처리 - productService.updateProduct( - inactiveMatch.productId(), - new UpdateProductRequest( - vendor.getId(), - inactiveMatch.name(), - inactiveMatch.code(), - inactiveMatch.unit(), - inactiveMatch.boxWeightG(), - inactiveMatch.unitPerBox(), - inactiveMatch.unitWeightG(), - false, - inactiveMatch.costPrice(), - inactiveMatch.retailPrice(), - inactiveMatch.wholesalePrice() - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, true, "고"); - - // then - assertThat(page.totalElements()).isEqualTo(1); - assertThat(page.content()).hasSize(1); - ProductResponse only = page.content().get(0); - assertThat(only.productId()).isEqualTo(activeMatch.productId()); - assertThat(only.name()).isEqualTo("고체치약"); - assertThat(only.isActivated()).isTrue(); - } - - @Test - void 품목_목록_조회시_다른_상점_품목은_포함되지_않는다() { - // given - Store store1 = newStore(); - Store store2 = newStore(); - - Vendor vendor1 = newVendor(store1); - Vendor vendor2 = newVendor(store2); - - User user1 = newUser(store1); - User user2 = userRepository.save( - User.builder() - .store(store2) - .username("tester_store2") - .password("password") - .name("상점2 관리자") - .role(UserRole.ADMIN) - .build() - ); - - // store1의 품목 2개 - productService.createProduct( - new CreateProductRequest( - vendor1.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user1.getId() - ); - productService.createProduct( - new CreateProductRequest( - vendor1.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user1.getId() - ); - - // store2의 품목 1개 - productService.createProduct( - new CreateProductRequest( - vendor2.getId(), - "세제", - "P-003", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 5, - BigDecimal.valueOf(200.0), - 3000, - 4000, - 3500 - ), - user2.getId() - ); - - // when - PageResponse page = - productService.getProductList(user1.getId(), 1, 10, null, null); - - // then - assertThat(page.totalElements()).isEqualTo(2); - assertThat(page.content()).hasSize(2); - assertThat(page.content()) - .extracting(ProductResponse::storeId) - .containsOnly(store1.getId()); - } - - @Test - void 존재하지_않는_사용자_품목_목록_조회시_예외가_발생한다() { - // given - Long notExistUserId = 9999L; - - // when & then - assertThatThrownBy(() -> - productService.getProductList(notExistUserId, 1, 10, null, null) - ) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); - } - - @Test - void 품목_목록_비활성_필터_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - // 활성 품목 - ProductResponse active = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - // 비활성로 바꿀 품목 - ProductResponse willBeInactive = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - - // 고무장갑 비활성 처리 - productService.updateProduct( - willBeInactive.productId(), - new UpdateProductRequest( - vendor.getId(), - willBeInactive.name(), - willBeInactive.code(), - willBeInactive.unit(), - willBeInactive.boxWeightG(), - willBeInactive.unitPerBox(), - willBeInactive.unitWeightG(), - false, - willBeInactive.costPrice(), - willBeInactive.retailPrice(), - willBeInactive.wholesalePrice() - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, false, null); - - // then - assertThat(page.totalElements()).isEqualTo(1); - assertThat(page.content()).hasSize(1); - ProductResponse only = page.content().get(0); - assertThat(only.productId()).isEqualTo(willBeInactive.productId()); - assertThat(only.isActivated()).isFalse(); - } - - @Test - void 품목_목록_비활성_및_이름검색_동시_필터링_조회에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - // 활성 + 이름 매칭 - productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - // 비활성 + 이름 매칭 - ProductResponse inactiveMatch = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고무장갑", - "P-002", - ProductUnit.EA, - null, - 1, - null, - 500, - 800, - 600 - ), - user.getId() - ); - - // 비활성 + 이름 미매칭 - ProductResponse inactiveNonMatch = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "세제", - "P-003", - ProductUnit.ML, - BigDecimal.valueOf(1000.0), - 5, - BigDecimal.valueOf(200.0), - 3000, - 4000, - 3500 - ), - user.getId() - ); - - // 고무장갑, 세제 둘 다 비활성 처리 - productService.updateProduct( - inactiveMatch.productId(), - new UpdateProductRequest( - vendor.getId(), - inactiveMatch.name(), - inactiveMatch.code(), - inactiveMatch.unit(), - inactiveMatch.boxWeightG(), - inactiveMatch.unitPerBox(), - inactiveMatch.unitWeightG(), - false, - inactiveMatch.costPrice(), - inactiveMatch.retailPrice(), - inactiveMatch.wholesalePrice() - ), - user.getId() - ); - - productService.updateProduct( - inactiveNonMatch.productId(), - new UpdateProductRequest( - vendor.getId(), - inactiveNonMatch.name(), - inactiveNonMatch.code(), - inactiveNonMatch.unit(), - inactiveNonMatch.boxWeightG(), - inactiveNonMatch.unitPerBox(), - inactiveNonMatch.unitWeightG(), - false, - inactiveNonMatch.costPrice(), - inactiveNonMatch.retailPrice(), - inactiveNonMatch.wholesalePrice() - ), - user.getId() - ); - - // when - PageResponse page = - productService.getProductList(user.getId(), 1, 10, false, "고"); - - // then - assertThat(page.totalElements()).isEqualTo(1); - assertThat(page.content()).hasSize(1); - ProductResponse only = page.content().get(0); - assertThat(only.productId()).isEqualTo(inactiveMatch.productId()); - assertThat(only.name()).isEqualTo("고무장갑"); - assertThat(only.isActivated()).isFalse(); - } - - @Test - void 품목_삭제에_성공한다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - ProductResponse created = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - Long productId = created.productId(); - - // when - DeleteProductResponse deleteResponse = productService.deleteProduct(productId, user.getId()); - - // then - assertThat(deleteResponse.success()).isTrue(); - - assertThatThrownBy(() -> productService.getProductDetail(productId, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); - - PageResponse page = - productService.getProductList(user.getId(), 1, 10, null, null); - - assertThat(page.totalElements()).isZero(); - assertThat(page.content()).isEmpty(); - } - - @Test - void 존재하지_않는_품목_삭제시_예외가_발생한다() { - // given - Store store = newStore(); - User user = newUser(store); - Long notExistProductId = 9999L; - - // when & then - assertThatThrownBy(() -> productService.deleteProduct(notExistProductId, user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); - } - - @Test - void 다른_상점의_품목을_삭제하려고_하면_예외가_발생한다() { - // given - Store store1 = newStore(); - Store store2 = newStore(); - - Vendor vendor1 = newVendor(store1); - Vendor vendor2 = newVendor(store2); - - User userOfStore1 = newUser(store1); - User userOfStore2 = userRepository.save( - User.builder() - .store(store2) - .username("delete_tester_store2") - .password("password") - .name("상점2 관리자(삭제)") - .role(UserRole.ADMIN) - .build() - ); - - ProductResponse productOfStore2 = productService.createProduct( - new CreateProductRequest( - vendor2.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - userOfStore2.getId() - ); - - // when & then - assertThatThrownBy(() -> productService.deleteProduct(productOfStore2.productId(), userOfStore1.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); - } - - @Test - void 삭제된_품목은_목록_및_상세에서_조회되지_않는다() { - // given - Store store = newStore(); - Vendor vendor = newVendor(store); - User user = newUser(store); - - ProductResponse product = productService.createProduct( - new CreateProductRequest( - vendor.getId(), - "고체치약", - "P-001", - ProductUnit.G, - BigDecimal.valueOf(900.0), - 10, - BigDecimal.valueOf(90.0), - 1000, - 1500, - 1200 - ), - user.getId() - ); - - productService.deleteProduct(product.productId(), user.getId()); - - // when & then - assertThatThrownBy(() -> productService.getProductDetail(product.productId(), user.getId())) - .isInstanceOf(BaseException.class) - .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); - - // when & then - PageResponse page = - productService.getProductList(user.getId(), 1, 10, null, null); - - assertThat(page.totalElements()).isZero(); - assertThat(page.content()).isEmpty(); - } + @Autowired + private ProductService productService; + @Autowired + private ProductRepository productRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private VendorRepository vendorRepository; + @Autowired + private InventoryRepository inventoryRepository; + + private Store newStore() { + return storeRepository.save( + Store.builder() + .name("테스트 상점") + .isActivate(true) + .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) + .build()); + } + + private Vendor newVendor(Store store) { + return vendorRepository.save( + Vendor.builder() + .store(store) + .name("테스트 발주처") + .channel(VendorChannel.KAKAO) + .contactPoint("010-0000-0000") + .note("테스트 메모") + .activated(true) + .build()); + } + + private User newUser(Store store) { + return userRepository.save( + User.builder() + .store(store) + .username("tester") + .password("password") + .name("테스트 유저") + .role(UserRole.ADMIN) + .build()); + } + + @Test + void 품목_생성에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + CreateProductRequest request = new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + // when + ProductResponse response = productService.createProduct(request, user.getId()); + + // then + assertThat(response.name()).isEqualTo("고체치약"); + assertThat(response.cafe24Code()).isEqualTo("C-001"); + assertThat(response.posCode()).isEqualTo("P-001"); + assertThat(response.unit()).isEqualTo(ProductUnit.G); + assertThat(response.boxWeightG()).isEqualByComparingTo("1000.0"); + assertThat(response.unitPerBox()).isEqualTo(10); + assertThat(response.unitWeightG()).isEqualByComparingTo("100.0"); + assertThat(response.costPrice()).isEqualTo(1000); + assertThat(response.retailPrice()).isEqualTo(1500); + assertThat(response.wholesalePrice()).isEqualTo(1200); + assertThat(response.storeId()).isEqualTo(store.getId()); + assertThat(response.vendorId()).isEqualTo(vendor.getId()); + assertThat(response.isActivated()).isTrue(); + + List inventories = inventoryRepository.findAll(); + assertThat(inventories).hasSize(1); + + Inventory inventory = inventories.get(0); + assertThat(inventory.getProduct().getId()).isEqualTo(response.productId()); + assertThat(inventory.getDisplayStock()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(inventory.getWarehouseStock()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(inventory.getIncomingReserved()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(inventory.getOutgoingReserved()).isEqualByComparingTo(BigDecimal.ZERO); + assertThat(inventory.getReorderTriggerPoint()) + .isEqualByComparingTo(store.getDefaultCountCheckThreshold()); + } + + @Test + void 사용자_존재하지_않으면_품목_생성시_예외가_발생한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + + Long notExistUserId = 9999L; + + CreateProductRequest request = new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + // when & then + assertThatThrownBy(() -> productService.createProduct(request, notExistUserId)) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 발주처가_존재하지_않으면_품목_생성시_예외가_발생한다() { + // given + Store store = newStore(); + User user = newUser(store); + + Long notExistVendorId = 9999L; + + CreateProductRequest request = new CreateProductRequest( + notExistVendorId, + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + // when & then + assertThatThrownBy(() -> productService.createProduct(request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage()); + } + + @Test + void 다른_상점의_발주처로_품목_생성시_예외가_발생한다() { + // given + Store store1 = newStore(); + Store store2 = storeRepository.save( + Store.builder() + .name("다른 상점") + .isActivate(true) + .defaultCountCheckThreshold(BigDecimal.valueOf(0.2)) + .build()); + + Vendor vendorOfStore2 = newVendor(store2); + User userOfStore1 = newUser(store1); + + CreateProductRequest request = new CreateProductRequest( + vendorOfStore2.getId(), // 다른 상점의 발주처! + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + 1000, + 1500, + 1200); + + // when & then + assertThatThrownBy(() -> productService.createProduct(request, userOfStore1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.VENDOR_ACCESS_DENIED.getMessage()); + } + + @Test + void 품목_수정에_성공한다() { + // given + Store store = newStore(); + Vendor vendor1 = newVendor(store); + Vendor vendor2 = newVendor(store); + User user = newUser(store); + + CreateProductRequest createRequest = new CreateProductRequest( + vendor1.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200); + + ProductResponse created = productService.createProduct(createRequest, user.getId()); + + // when + UpdateProductRequest updateRequest = new UpdateProductRequest( + vendor2.getId(), + "수정된 고체치약", + "C-999", + "P-999", + ProductUnit.ML, + BigDecimal.valueOf(1200.0), + 20, + BigDecimal.valueOf(110.0), + false, + 2000, + 2500, + 2200); + + ProductResponse updated = productService.updateProduct(created.productId(), updateRequest, + user.getId()); + + // then + assertThat(updated.name()).isEqualTo("수정된 고체치약"); + assertThat(updated.cafe24Code()).isEqualTo("C-999"); + assertThat(updated.posCode()).isEqualTo("P-999"); + assertThat(updated.unit()).isEqualTo(ProductUnit.ML); + assertThat(updated.boxWeightG()).isEqualByComparingTo("1200.0"); + assertThat(updated.unitPerBox()).isEqualTo(20); + assertThat(updated.unitWeightG()).isEqualByComparingTo("110.0"); + assertThat(updated.isActivated()).isFalse(); + assertThat(updated.costPrice()).isEqualTo(2000); + assertThat(updated.retailPrice()).isEqualTo(2500); + assertThat(updated.wholesalePrice()).isEqualTo(2200); + assertThat(updated.vendorId()).isEqualTo(vendor2.getId()); + } + + @Test + void 존재하지_않는_품목을_수정하려고_하면_예외가_발생한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + Long notExistProductId = 9999L; + + UpdateProductRequest request = new UpdateProductRequest( + vendor.getId(), + "변경 이름", + "C-CODE", + "P-CODE", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + true, + 1000, + 2000, + 1500); + + // when & then + assertThatThrownBy(() -> productService.updateProduct(notExistProductId, request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + void 존재하지_않는_발주처로_수정시_예외가_발생한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + ProductResponse created = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + Long notExistVendorId = 9999L; + + UpdateProductRequest request = new UpdateProductRequest( + notExistVendorId, + "변경됨", + "C-NEW", + "P-NEW", + ProductUnit.G, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + true, + 2000, + 3000, + 2500); + + // when & then + assertThatThrownBy(() -> productService.updateProduct(created.productId(), request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage()); + } + + @Test + void 다른_상점의_발주처로_수정하면_예외가_발생한다() { + // given + Store store1 = newStore(); + Store store2 = newStore(); + + Vendor vendor1 = newVendor(store1); + Vendor vendorOfStore2 = newVendor(store2); + User user = newUser(store1); + + ProductResponse created = productService.createProduct( + new CreateProductRequest( + vendor1.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + UpdateProductRequest request = new UpdateProductRequest( + vendorOfStore2.getId(), + "변경됨", + "C-NEW", + "P-NEW", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + true, + 2000, + 3000, + 2500); + + // when & then + assertThatThrownBy(() -> productService.updateProduct(created.productId(), request, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.VENDOR_ACCESS_DENIED.getMessage()); + } + + @Test + void 다른_상점의_품목을_수정하려고_하면_예외가_발생한다() { + // given + Store store1 = newStore(); + Store store2 = newStore(); + + Vendor vendorOfStore1 = newVendor(store1); + Vendor vendorOfStore2 = newVendor(store2); + + User userOfStore1 = newUser(store1); + User userOfStore2 = userRepository.save( + User.builder() + .store(store2) + .username("tester_store2") + .password("password") + .name("상점2 관리자") + .role(UserRole.ADMIN) + .build()); + + ProductResponse productOfStore2 = productService.createProduct( + new CreateProductRequest( + vendorOfStore2.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + userOfStore2.getId()); + + // when & then + UpdateProductRequest request = new UpdateProductRequest( + vendorOfStore1.getId(), + "변경됨", + "C-NEW", + "P-NEW", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 10, + BigDecimal.valueOf(100.0), + true, + 2000, + 3000, + 2500); + + assertThatThrownBy(() -> productService.updateProduct(productOfStore2.productId(), request, + userOfStore1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); + } + + @Test + void 품목_상세_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + ProductResponse created = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + // when + ProductResponse detail = productService.getProductDetail(created.productId(), user.getId()); + + // then + assertThat(detail.productId()).isEqualTo(created.productId()); + assertThat(detail.name()).isEqualTo("고체치약"); + assertThat(detail.cafe24Code()).isEqualTo("C-001"); + assertThat(detail.posCode()).isEqualTo("P-001"); + assertThat(detail.unit()).isEqualTo(ProductUnit.G); + assertThat(detail.boxWeightG()).isEqualByComparingTo("900.0"); + assertThat(detail.unitPerBox()).isEqualTo(10); + assertThat(detail.unitWeightG()).isEqualByComparingTo("90.0"); + assertThat(detail.costPrice()).isEqualTo(1000); + assertThat(detail.retailPrice()).isEqualTo(1500); + assertThat(detail.wholesalePrice()).isEqualTo(1200); + assertThat(detail.storeId()).isEqualTo(store.getId()); + assertThat(detail.vendorId()).isEqualTo(vendor.getId()); + assertThat(detail.isActivated()).isTrue(); + } + + @Test + void 존재하지_않는_품목_상세_조회시_예외가_발생한다() { + // given + Store store = newStore(); + User user = newUser(store); + Long notExistProductId = 9999L; + + // when & then + assertThatThrownBy(() -> productService.getProductDetail(notExistProductId, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + void 다른_상점의_품목을_상세_조회하면_예외가_발생한다() { + // given + Store store1 = newStore(); + Store store2 = newStore(); + + Vendor vendorOfStore2 = newVendor(store2); + + User userOfStore1 = newUser(store1); + User userOfStore2 = userRepository.save( + User.builder() + .store(store2) + .username("detail_tester_store2") + .password("password") + .name("상점2 관리자(상세조회)") + .role(UserRole.ADMIN) + .build()); + + ProductResponse productOfStore2 = productService.createProduct( + new CreateProductRequest( + vendorOfStore2.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + userOfStore2.getId()); + + // when & then + assertThatThrownBy(() -> productService.getProductDetail(productOfStore2.productId(), + userOfStore1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); + } + + @Test + void 품목_목록_전체_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "C-002", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "세제", + "C-003", + "P-003", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 5, + BigDecimal.valueOf(200.0), + 3000, + 4000, + 3500), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, null, null); + + // then + assertThat(page.page()).isEqualTo(1); + assertThat(page.size()).isEqualTo(10); + assertThat(page.totalElements()).isEqualTo(3); + assertThat(page.content()).hasSize(3); + } + + @Test + void 품목_목록_활성_필터_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + // 활성 품목 + ProductResponse active = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + // 비활성으로 바꿀 품목 + ProductResponse willBeInactive = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "C-002", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + + // 고무장갑 비활성 처리 + productService.updateProduct( + willBeInactive.productId(), + new UpdateProductRequest( + vendor.getId(), + willBeInactive.name(), + willBeInactive.cafe24Code(), + willBeInactive.posCode(), + willBeInactive.unit(), + willBeInactive.boxWeightG(), + willBeInactive.unitPerBox(), + willBeInactive.unitWeightG(), + false, + willBeInactive.costPrice(), + willBeInactive.retailPrice(), + willBeInactive.wholesalePrice()), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, true, null); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + assertThat(page.content().get(0).productId()).isEqualTo(active.productId()); + assertThat(page.content().get(0).isActivated()).isTrue(); + } + + @Test + void 품목_목록_이름검색으로_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "C-002", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "세제", + "C-003", + "P-003", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 5, + BigDecimal.valueOf(200.0), + 3000, + 4000, + 3500), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, null, "고"); + + // then + assertThat(page.totalElements()).isEqualTo(2); + assertThat(page.content()).hasSize(2); + assertThat(page.content()) + .extracting(ProductResponse::name) + .allMatch(name -> name.contains("고")); + } + + @Test + void 품목_목록_활성_및_이름검색_동시_필터링_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + ProductResponse activeMatch = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "C-001", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + ProductResponse inactiveMatch = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "C-002", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + + // 고무장갑 비활성 처리 + productService.updateProduct( + inactiveMatch.productId(), + new UpdateProductRequest( + vendor.getId(), + inactiveMatch.name(), + inactiveMatch.cafe24Code(), + inactiveMatch.posCode(), + inactiveMatch.unit(), + inactiveMatch.boxWeightG(), + inactiveMatch.unitPerBox(), + inactiveMatch.unitWeightG(), + false, + inactiveMatch.costPrice(), + inactiveMatch.retailPrice(), + inactiveMatch.wholesalePrice()), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, true, "고"); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + ProductResponse only = page.content().get(0); + assertThat(only.productId()).isEqualTo(activeMatch.productId()); + assertThat(only.name()).isEqualTo("고체치약"); + assertThat(only.isActivated()).isTrue(); + } + + @Test + void 품목_목록_조회시_다른_상점_품목은_포함되지_않는다() { + // given + Store store1 = newStore(); + Store store2 = newStore(); + + Vendor vendor1 = newVendor(store1); + Vendor vendor2 = newVendor(store2); + + User user1 = newUser(store1); + User user2 = userRepository.save( + User.builder() + .store(store2) + .username("tester_store2") + .password("password") + .name("상점2 관리자") + .role(UserRole.ADMIN) + .build()); + + // store1의 품목 2개 + productService.createProduct( + new CreateProductRequest( + vendor1.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user1.getId()); + productService.createProduct( + new CreateProductRequest( + vendor1.getId(), + "고무장갑", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user1.getId()); + + // store2의 품목 1개 + productService.createProduct( + new CreateProductRequest( + vendor2.getId(), + "세제", + "P-003", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 5, + BigDecimal.valueOf(200.0), + 3000, + 4000, + 3500), + user2.getId()); + + // when + PageResponse page = productService.getProductList(user1.getId(), 1, 10, null, null); + + // then + assertThat(page.totalElements()).isEqualTo(2); + assertThat(page.content()).hasSize(2); + assertThat(page.content()) + .extracting(ProductResponse::storeId) + .containsOnly(store1.getId()); + } + + @Test + void 존재하지_않는_사용자_품목_목록_조회시_예외가_발생한다() { + // given + Long notExistUserId = 9999L; + + // when & then + assertThatThrownBy(() -> productService.getProductList(notExistUserId, 1, 10, null, null)) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 품목_목록_비활성_필터_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + // 활성 품목 + ProductResponse active = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + // 비활성로 바꿀 품목 + ProductResponse willBeInactive = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + + // 고무장갑 비활성 처리 + productService.updateProduct( + willBeInactive.productId(), + new UpdateProductRequest( + vendor.getId(), + willBeInactive.name(), + willBeInactive.code(), + willBeInactive.unit(), + willBeInactive.boxWeightG(), + willBeInactive.unitPerBox(), + willBeInactive.unitWeightG(), + false, + willBeInactive.costPrice(), + willBeInactive.retailPrice(), + willBeInactive.wholesalePrice()), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, false, null); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + ProductResponse only = page.content().get(0); + assertThat(only.productId()).isEqualTo(willBeInactive.productId()); + assertThat(only.isActivated()).isFalse(); + } + + @Test + void 품목_목록_비활성_및_이름검색_동시_필터링_조회에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + // 활성 + 이름 매칭 + productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + // 비활성 + 이름 매칭 + ProductResponse inactiveMatch = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고무장갑", + "P-002", + ProductUnit.EA, + null, + 1, + null, + 500, + 800, + 600), + user.getId()); + + // 비활성 + 이름 미매칭 + ProductResponse inactiveNonMatch = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "세제", + "P-003", + ProductUnit.ML, + BigDecimal.valueOf(1000.0), + 5, + BigDecimal.valueOf(200.0), + 3000, + 4000, + 3500), + user.getId()); + + // 고무장갑, 세제 둘 다 비활성 처리 + productService.updateProduct( + inactiveMatch.productId(), + new UpdateProductRequest( + vendor.getId(), + inactiveMatch.name(), + inactiveMatch.code(), + inactiveMatch.unit(), + inactiveMatch.boxWeightG(), + inactiveMatch.unitPerBox(), + inactiveMatch.unitWeightG(), + false, + inactiveMatch.costPrice(), + inactiveMatch.retailPrice(), + inactiveMatch.wholesalePrice()), + user.getId()); + + productService.updateProduct( + inactiveNonMatch.productId(), + new UpdateProductRequest( + vendor.getId(), + inactiveNonMatch.name(), + inactiveNonMatch.code(), + inactiveNonMatch.unit(), + inactiveNonMatch.boxWeightG(), + inactiveNonMatch.unitPerBox(), + inactiveNonMatch.unitWeightG(), + false, + inactiveNonMatch.costPrice(), + inactiveNonMatch.retailPrice(), + inactiveNonMatch.wholesalePrice()), + user.getId()); + + // when + PageResponse page = productService.getProductList(user.getId(), 1, 10, false, "고"); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + ProductResponse only = page.content().get(0); + assertThat(only.productId()).isEqualTo(inactiveMatch.productId()); + assertThat(only.name()).isEqualTo("고무장갑"); + assertThat(only.isActivated()).isFalse(); + } + + @Test + void 품목_삭제에_성공한다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + ProductResponse created = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + Long productId = created.productId(); + + // when + DeleteProductResponse deleteResponse = productService.deleteProduct(productId, user.getId()); + + // then + assertThat(deleteResponse.success()).isTrue(); + + assertThatThrownBy(() -> productService.getProductDetail(productId, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + + PageResponse page = productService.getProductList(user.getId(), 1, 10, null, null); + + assertThat(page.totalElements()).isZero(); + assertThat(page.content()).isEmpty(); + } + + @Test + void 존재하지_않는_품목_삭제시_예외가_발생한다() { + // given + Store store = newStore(); + User user = newUser(store); + Long notExistProductId = 9999L; + + // when & then + assertThatThrownBy(() -> productService.deleteProduct(notExistProductId, user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + void 다른_상점의_품목을_삭제하려고_하면_예외가_발생한다() { + // given + Store store1 = newStore(); + Store store2 = newStore(); + + Vendor vendor1 = newVendor(store1); + Vendor vendor2 = newVendor(store2); + + User userOfStore1 = newUser(store1); + User userOfStore2 = userRepository.save( + User.builder() + .store(store2) + .username("delete_tester_store2") + .password("password") + .name("상점2 관리자(삭제)") + .role(UserRole.ADMIN) + .build()); + + ProductResponse productOfStore2 = productService.createProduct( + new CreateProductRequest( + vendor2.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + userOfStore2.getId()); + + // when & then + assertThatThrownBy( + () -> productService.deleteProduct(productOfStore2.productId(), userOfStore1.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.STORE_ACCESS_DENIED.getMessage()); + } + + @Test + void 삭제된_품목은_목록_및_상세에서_조회되지_않는다() { + // given + Store store = newStore(); + Vendor vendor = newVendor(store); + User user = newUser(store); + + ProductResponse product = productService.createProduct( + new CreateProductRequest( + vendor.getId(), + "고체치약", + "P-001", + ProductUnit.G, + BigDecimal.valueOf(900.0), + 10, + BigDecimal.valueOf(90.0), + 1000, + 1500, + 1200), + user.getId()); + + productService.deleteProduct(product.productId(), user.getId()); + + // when & then + assertThatThrownBy(() -> productService.getProductDetail(product.productId(), user.getId())) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + + // when & then + PageResponse page = productService.getProductList(user.getId(), 1, 10, null, null); + + assertThat(page.totalElements()).isZero(); + assertThat(page.content()).isEmpty(); + } } diff --git a/src/test/java/com/almang/inventory/retail/service/RetailServiceTest.java b/src/test/java/com/almang/inventory/retail/service/RetailServiceTest.java new file mode 100644 index 00000000..b93bcef1 --- /dev/null +++ b/src/test/java/com/almang/inventory/retail/service/RetailServiceTest.java @@ -0,0 +1,110 @@ +package com.almang.inventory.retail.service; + +import com.almang.inventory.inventory.domain.Inventory; +import com.almang.inventory.inventory.repository.InventoryRepository; +import com.almang.inventory.product.domain.Product; +import com.almang.inventory.product.repository.ProductRepository; +import com.almang.inventory.retail.domain.Retail; +import com.almang.inventory.retail.repository.RetailRepository; +import com.almang.inventory.store.domain.Store; +import com.almang.inventory.store.repository.StoreRepository; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.DisplayName; +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.mock.web.MockMultipartFile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class RetailServiceTest { + + @InjectMocks + private RetailService retailService; + + @Mock + private RetailRepository retailRepository; + + @Mock + private ProductRepository productRepository; + + @Mock + private StoreRepository storeRepository; + + @Mock + private InventoryRepository inventoryRepository; + + @Test + @DisplayName("Excel file processing success") + void processRetailExcel_Success() throws IOException { + // Given + // 1. Mock Store + Store store = Store.builder().id(1L).name("Test Store").build(); + given(storeRepository.findAll()).willReturn(List.of(store)); + + // 2. Mock Product + String productCode = "P001"; + Product product = Product.builder().id(1L).posCode(productCode).name("Test Product").build(); + given(productRepository.findByPosCode(productCode)).willReturn(Optional.of(product)); + + // 3. Mock Inventory + Inventory inventory = Inventory.builder().id(1L).product(product).displayStock(BigDecimal.valueOf(100)).build(); + given(inventoryRepository.findByProduct(product)).willReturn(Optional.of(inventory)); + + // 4. Create Excel File + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Sheet1"); + + // Header + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("No."); + header.createCell(1).setCellValue("Product Code"); + header.createCell(2).setCellValue("Product Name"); + header.createCell(3).setCellValue("Quantity"); + header.createCell(4).setCellValue("Real Sales"); + + // Data Row + Row row = sheet.createRow(1); + row.createCell(0).setCellValue(1); + row.createCell(1).setCellValue(productCode); + row.createCell(2).setCellValue("Test Product"); + row.createCell(3).setCellValue(10); // Quantity 10 + row.createCell(4).setCellValue(10000); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + workbook.write(bos); + workbook.close(); + + MockMultipartFile file = new MockMultipartFile( + "file", + "test.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + bos.toByteArray()); + + // When + retailService.processRetailExcel(file); + + // Then + verify(retailRepository, times(1)).saveAll(anyList()); + // Inventory decrease check is tricky because it's a void method on the entity + // or service logic inside lambda + // But we can verify inventoryRepository.findByProduct was called + verify(inventoryRepository, times(1)).findByProduct(product); + } +}