|
|
@@ -0,0 +1,263 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.dto.CreateProductOrderDTO;
|
|
|
+import com.etotem.cfc.dto.OrderItemVO;
|
|
|
+import com.etotem.cfc.dto.ProductOrderDTO;
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
+import com.etotem.cfc.entity.ProductOrder;
|
|
|
+import com.etotem.cfc.entity.SpecialPriceDeal;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.ProductMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductOrderMapper;
|
|
|
+import com.etotem.cfc.mapper.SpecialPriceDealMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyInt;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 特价下单分支 + 回滚挂载点测试。
|
|
|
+ * 模式参考 ProductOrderServiceUpdateExpressFieldTest(Mockito + ReflectionTestUtils,不起 Spring 上下文)。
|
|
|
+ *
|
|
|
+ * SpecialPriceService 用真实实例 + mock 的 SpecialPriceDealMapper(plan 要求:单独 mock
|
|
|
+ * SpecialPriceDealMapper 供 consumeQuota / userPurchased / selectById 桩)。
|
|
|
+ */
|
|
|
+public class ProductOrderServiceSpecialDealTest {
|
|
|
+
|
|
|
+ private static final long DEAL_ID = 1L;
|
|
|
+ private static final long PRODUCT_ID = 100L;
|
|
|
+ private static final long BUYER_ID = 200L;
|
|
|
+ private static final String ORDER_NO = "O20261021TEST";
|
|
|
+
|
|
|
+ private ProductOrderService orderService;
|
|
|
+ private SpecialPriceDealMapper specialDealMapper;
|
|
|
+ private ProductMapper productMapper;
|
|
|
+ private UserMapper userMapper;
|
|
|
+ private ProductOrderMapper orderMapper;
|
|
|
+ private ProductService productService;
|
|
|
+ private InventoryService inventoryService;
|
|
|
+ private CouponService couponService;
|
|
|
+ private ProductGiftRuleService productGiftRuleService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ public void setUp() {
|
|
|
+ orderService = new ProductOrderService();
|
|
|
+ SpecialPriceService specialPriceService = new SpecialPriceService();
|
|
|
+ specialDealMapper = mock(SpecialPriceDealMapper.class);
|
|
|
+ ReflectionTestUtils.setField(specialPriceService, "specialPriceDealMapper", specialDealMapper);
|
|
|
+ productMapper = mock(ProductMapper.class);
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
+ orderMapper = mock(ProductOrderMapper.class);
|
|
|
+ productService = mock(ProductService.class);
|
|
|
+ inventoryService = mock(InventoryService.class);
|
|
|
+ couponService = mock(CouponService.class);
|
|
|
+ productGiftRuleService = mock(ProductGiftRuleService.class);
|
|
|
+ ReflectionTestUtils.setField(orderService, "specialPriceService", specialPriceService);
|
|
|
+ ReflectionTestUtils.setField(orderService, "productMapper", productMapper);
|
|
|
+ ReflectionTestUtils.setField(orderService, "userMapper", userMapper);
|
|
|
+ ReflectionTestUtils.setField(orderService, "orderMapper", orderMapper);
|
|
|
+ ReflectionTestUtils.setField(orderService, "productService", productService);
|
|
|
+ ReflectionTestUtils.setField(orderService, "inventoryService", inventoryService);
|
|
|
+ ReflectionTestUtils.setField(orderService, "couponService", couponService);
|
|
|
+ ReflectionTestUtils.setField(orderService, "productGiftRuleService", productGiftRuleService);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== helpers ====================
|
|
|
+
|
|
|
+ private SpecialPriceDeal activeDeal(int sold, int perUser) {
|
|
|
+ SpecialPriceDeal deal = new SpecialPriceDeal();
|
|
|
+ deal.setId(DEAL_ID);
|
|
|
+ deal.setProductId(PRODUCT_ID);
|
|
|
+ deal.setName("开学季特价");
|
|
|
+ deal.setSpecialPrice(9900);
|
|
|
+ deal.setStartTime(new Date(System.currentTimeMillis() - 86400_000L));
|
|
|
+ deal.setEndTime(new Date(System.currentTimeMillis() + 86400_000L));
|
|
|
+ deal.setTotalQuantity(100);
|
|
|
+ deal.setSoldQuantity(sold);
|
|
|
+ deal.setPerUserLimit(perUser);
|
|
|
+ deal.setAllowCoupon(0);
|
|
|
+ deal.setStatus("active");
|
|
|
+ return deal;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Product onShelfProduct() {
|
|
|
+ Product product = new Product();
|
|
|
+ product.setId(PRODUCT_ID);
|
|
|
+ product.setName("DAN测评任务模板");
|
|
|
+ product.setPrice(19800);
|
|
|
+ product.setProductType("virtual");
|
|
|
+ product.setStatus("on_shelf");
|
|
|
+ product.setCoverImage("http://example.com/a.png");
|
|
|
+ return product;
|
|
|
+ }
|
|
|
+
|
|
|
+ private User buyer() {
|
|
|
+ User user = new User();
|
|
|
+ user.setId(BUYER_ID);
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CreateProductOrderDTO dealDTO(int quantity) {
|
|
|
+ CreateProductOrderDTO dto = new CreateProductOrderDTO();
|
|
|
+ dto.setSpecialDealId(DEAL_ID);
|
|
|
+ dto.setProductId(PRODUCT_ID);
|
|
|
+ dto.setQuantity(quantity);
|
|
|
+ dto.setAddressSnapshot("{\"receiverName\":\"张三\",\"phone\":\"13800000000\"}");
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 特价下单分支 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void create_withSpecialDealId_pricesAtSpecialPrice_consumesQuotaAndInserts() {
|
|
|
+ when(specialDealMapper.selectById(DEAL_ID)).thenReturn(activeDeal(0, 2));
|
|
|
+ when(specialDealMapper.userPurchased(DEAL_ID, BUYER_ID)).thenReturn(0);
|
|
|
+ when(productMapper.selectById(PRODUCT_ID)).thenReturn(onShelfProduct());
|
|
|
+ when(userMapper.selectById(BUYER_ID)).thenReturn(buyer());
|
|
|
+ when(specialDealMapper.consumeQuota(DEAL_ID, 2)).thenReturn(1);
|
|
|
+
|
|
|
+ Result<ProductOrderDTO> result = orderService.create(dealDTO(2), BUYER_ID);
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ ArgumentCaptor<ProductOrder> captor = ArgumentCaptor.forClass(ProductOrder.class);
|
|
|
+ verify(orderMapper).insert(captor.capture());
|
|
|
+ ProductOrder order = captor.getValue();
|
|
|
+ // 特价覆盖会员价:unitPrice/totalAmount 按特价
|
|
|
+ assertEquals(PRODUCT_ID, order.getProductId());
|
|
|
+ assertEquals(DEAL_ID, order.getSpecialDealId());
|
|
|
+ assertEquals(9900, order.getUnitPrice());
|
|
|
+ assertEquals(19800, order.getTotalAmount());
|
|
|
+ assertEquals(2, order.getQuantity());
|
|
|
+ assertEquals("pending", order.getStatus());
|
|
|
+ verify(specialDealMapper).consumeQuota(DEAL_ID, 2);
|
|
|
+ verify(specialDealMapper).userPurchased(DEAL_ID, BUYER_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void create_whenQuotaConsumeFails_returnsErrorAndDoesNotInsert() {
|
|
|
+ when(specialDealMapper.selectById(DEAL_ID)).thenReturn(activeDeal(100, 2)); // sold=total -> SOLD_OUT
|
|
|
+ when(specialDealMapper.userPurchased(DEAL_ID, BUYER_ID)).thenReturn(0);
|
|
|
+ when(productMapper.selectById(PRODUCT_ID)).thenReturn(onShelfProduct());
|
|
|
+ when(userMapper.selectById(BUYER_ID)).thenReturn(buyer());
|
|
|
+ when(specialDealMapper.consumeQuota(DEAL_ID, 2)).thenReturn(0); // 原子扣量失败(并发售罄)
|
|
|
+
|
|
|
+ Result<ProductOrderDTO> result = orderService.create(dealDTO(2), BUYER_ID);
|
|
|
+
|
|
|
+ assertNotEquals(200, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("售罄"));
|
|
|
+ verify(orderMapper, never()).insert(any(ProductOrder.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void create_overPerUserLimit_returnsLimitErrorBeforeQuota() {
|
|
|
+ when(specialDealMapper.selectById(DEAL_ID)).thenReturn(activeDeal(0, 2));
|
|
|
+ when(specialDealMapper.userPurchased(DEAL_ID, BUYER_ID)).thenReturn(5); // 已购5件,限2件
|
|
|
+
|
|
|
+ Result<ProductOrderDTO> result = orderService.create(dealDTO(2), BUYER_ID);
|
|
|
+
|
|
|
+ assertNotEquals(200, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("每人限购2件,您已购买5件"));
|
|
|
+ // 先查再扣:限购失败不应触发扣量,也不插入订单
|
|
|
+ verify(specialDealMapper, never()).consumeQuota(any(), anyInt());
|
|
|
+ verify(orderMapper, never()).insert(any(ProductOrder.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void create_couponNotAllowed_rejected() {
|
|
|
+ when(specialDealMapper.selectById(DEAL_ID)).thenReturn(activeDeal(0, 2));
|
|
|
+ when(specialDealMapper.userPurchased(DEAL_ID, BUYER_ID)).thenReturn(0);
|
|
|
+ CreateProductOrderDTO dto = dealDTO(2);
|
|
|
+ dto.setCouponId(9L); // allowCoupon=0
|
|
|
+
|
|
|
+ Result<ProductOrderDTO> result = orderService.create(dto, BUYER_ID);
|
|
|
+
|
|
|
+ assertNotEquals(200, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("不可使用优惠券"));
|
|
|
+ verify(specialDealMapper, never()).consumeQuota(any(), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void create_withItems_rejected_specialDealMustBeSingleProduct() {
|
|
|
+ CreateProductOrderDTO dto = dealDTO(2);
|
|
|
+ dto.setItems(Collections.singletonList(new OrderItemVO()));
|
|
|
+
|
|
|
+ Result<ProductOrderDTO> result = orderService.create(dto, BUYER_ID);
|
|
|
+
|
|
|
+ assertNotEquals(200, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("单独购买"));
|
|
|
+ verify(specialDealMapper, never()).consumeQuota(any(), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 回滚挂载 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void cancel_releasesQuotaForSpecialDealOrder() {
|
|
|
+ ProductOrder order = specialDealOrder();
|
|
|
+ order.setStatus("pending");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ Result<String> result = orderService.cancel(ORDER_NO, BUYER_ID);
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ assertEquals("cancelled", order.getStatus());
|
|
|
+ verify(specialDealMapper).releaseQuota(DEAL_ID, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void approveRefund_releasesQuotaForSpecialDealOrder() {
|
|
|
+ ProductOrder order = specialDealOrder();
|
|
|
+ order.setStatus("paid");
|
|
|
+ order.setRefundStatus(1);
|
|
|
+ order.setMoneyAmount(0); // 实付0:跳过微信退款分支,直接置 refunded
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ Result<String> result = orderService.approveRefund(ORDER_NO, 500L);
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ assertEquals("refunded", order.getStatus());
|
|
|
+ verify(specialDealMapper).releaseQuota(DEAL_ID, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void reviewUpdate_specialDealOrder_rejectedAtStart() {
|
|
|
+ ProductOrder order = specialDealOrder();
|
|
|
+ order.setStatus("pending");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
+ params.put("orderNo", ORDER_NO);
|
|
|
+ Result<Map<String, Object>> result = orderService.reviewUpdate(params, 300L);
|
|
|
+
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("特价订单不允许修改价格"));
|
|
|
+ verify(orderMapper, never()).updateById(any(ProductOrder.class));
|
|
|
+ verify(productMapper, never()).selectById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ private ProductOrder specialDealOrder() {
|
|
|
+ ProductOrder order = new ProductOrder();
|
|
|
+ order.setOrderNo(ORDER_NO);
|
|
|
+ order.setBuyerId(BUYER_ID);
|
|
|
+ order.setProductId(PRODUCT_ID);
|
|
|
+ order.setQuantity(2);
|
|
|
+ order.setSpecialDealId(DEAL_ID);
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+}
|