|
|
@@ -1,396 +0,0 @@
|
|
|
-package com.ruoyi.app.order;
|
|
|
-
|
|
|
-import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
-import static org.mockito.ArgumentMatchers.any;
|
|
|
-import static org.mockito.ArgumentMatchers.anyString;
|
|
|
-import static org.mockito.ArgumentMatchers.eq;
|
|
|
-import static org.mockito.Mockito.lenient;
|
|
|
-import static org.mockito.Mockito.never;
|
|
|
-import static org.mockito.Mockito.verify;
|
|
|
-import static org.mockito.Mockito.when;
|
|
|
-
|
|
|
-import java.math.BigDecimal;
|
|
|
-import java.time.LocalDate;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.List;
|
|
|
-
|
|
|
-import org.junit.jupiter.api.AfterEach;
|
|
|
-import org.junit.jupiter.api.BeforeEach;
|
|
|
-import org.junit.jupiter.api.Test;
|
|
|
-import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
-import org.mockito.ArgumentCaptor;
|
|
|
-import org.mockito.InjectMocks;
|
|
|
-import org.mockito.Mock;
|
|
|
-import org.mockito.MockedStatic;
|
|
|
-import org.mockito.Spy;
|
|
|
-import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
-
|
|
|
-import com.alibaba.fastjson.JSON;
|
|
|
-import com.alibaba.fastjson.JSONArray;
|
|
|
-import com.ruoyi.app.order.dto.OrderCreateInput;
|
|
|
-import com.ruoyi.app.order.dto.OrderCreatItem;
|
|
|
-import com.ruoyi.app.pay.LinePayRefundService;
|
|
|
-import com.ruoyi.app.utils.event.PushEventService;
|
|
|
-import com.ruoyi.common.core.domain.AjaxResult;
|
|
|
-import com.ruoyi.common.exception.ServiceException;
|
|
|
-import com.ruoyi.common.utils.MessageUtils;
|
|
|
-import com.ruoyi.system.domain.InfoAddress;
|
|
|
-import com.ruoyi.system.domain.InfoUser;
|
|
|
-import com.ruoyi.system.domain.OperatingHours;
|
|
|
-import com.ruoyi.system.domain.OrderParent;
|
|
|
-import com.ruoyi.system.domain.PosFood;
|
|
|
-import com.ruoyi.system.domain.PosOrder;
|
|
|
-import com.ruoyi.system.domain.PosOrderPromotion;
|
|
|
-import com.ruoyi.system.domain.PosStore;
|
|
|
-import com.ruoyi.system.dto.PromotionCalcRequest;
|
|
|
-import com.ruoyi.system.dto.PromotionCalcResponse;
|
|
|
-import com.ruoyi.system.dto.PromotionCalcResponse.PromotionDetail;
|
|
|
-import com.ruoyi.system.mapper.FoodSpecsValueMapper;
|
|
|
-import com.ruoyi.system.mapper.PosFoodMapper;
|
|
|
-import com.ruoyi.system.mapper.PosOrderPromotionMapper;
|
|
|
-import com.ruoyi.system.service.IInfoAddressService;
|
|
|
-import com.ruoyi.system.service.IInfoInvoiceService;
|
|
|
-import com.ruoyi.system.service.IInfoUserService;
|
|
|
-import com.ruoyi.system.service.IOperatingHoursService;
|
|
|
-import com.ruoyi.system.service.IOrderParentService;
|
|
|
-import com.ruoyi.system.service.IPosOrderLinePaymentService;
|
|
|
-import com.ruoyi.system.service.IPosOrderService;
|
|
|
-import com.ruoyi.system.service.IPosStoreService;
|
|
|
-import com.ruoyi.system.service.IPromotionCalcService;
|
|
|
-import com.ruoyi.system.service.IPromotionUserCouponService;
|
|
|
-import com.ruoyi.system.service.IUserFootprintService;
|
|
|
-import com.ruoyi.system.service.PaymentMethodGateService;
|
|
|
-import com.ruoyi.system.service.StallQualificationGateService;
|
|
|
-import com.ruoyi.system.utils.JwtUtil;
|
|
|
-import com.ruoyi.system.utils.OrderLogHelper;
|
|
|
-
|
|
|
-/**
|
|
|
- * T076 版本2下单闭环:父子金额服务端固化、报价差异拒绝回滚、多店/非外送拒绝、
|
|
|
- * 零实减券不核销、旧请求保持原行为。
|
|
|
- */
|
|
|
-@ExtendWith(MockitoExtension.class)
|
|
|
-class UserOrderFreeDeliveryCreateTest
|
|
|
-{
|
|
|
- private static final Long USER_ID = 9L;
|
|
|
- private static final Long STORE_ID = 1L;
|
|
|
- private static final Long ADDRESS_ID = 8L;
|
|
|
- private static final Long FOOD_ID = 10L;
|
|
|
- private static final String DD_ID = "DD10086";
|
|
|
-
|
|
|
- @Mock private IOrderParentService orderParentService;
|
|
|
- @Mock private PaymentMethodGateService paymentMethodGateService;
|
|
|
- @Mock private IPosOrderService posOrderService;
|
|
|
- @Mock private IPosStoreService posStoreService;
|
|
|
- @Mock private StallQualificationGateService stallQualificationGateService;
|
|
|
- @Mock private IInfoAddressService infoAddressService;
|
|
|
- @Mock private IOperatingHoursService operatingHoursService;
|
|
|
- @Mock private IUserFootprintService userFootprintService;
|
|
|
- @Mock private IInfoUserService infoUserService;
|
|
|
- @Mock private PushEventService pushEventService;
|
|
|
- @Mock private OrderLogHelper orderLogHelper;
|
|
|
- @Mock private PosOrderPromotionMapper posOrderPromotionMapper;
|
|
|
- @Mock private OrderInvoiceService orderInvoiceService;
|
|
|
- @Mock private IInfoInvoiceService infoInvoiceService;
|
|
|
- @Mock private IPosOrderLinePaymentService linePaymentService;
|
|
|
- @Mock private LinePayRefundService linePayRefundService;
|
|
|
- @Mock private OrderLifecycleService orderLifecycleService;
|
|
|
- @Mock private DeliveryOrderNotificationService deliveryOrderNotificationService;
|
|
|
- @Mock private MerchantNotificationRouter merchantNotificationRouter;
|
|
|
- @Mock private SelfDeliveryService selfDeliveryService;
|
|
|
- @Mock private FoodDeliveryPromotionQuoteService foodDeliveryPromotionQuoteService;
|
|
|
- @Mock private IPromotionCalcService promotionCalcService;
|
|
|
- @Mock private IPromotionUserCouponService promotionUserCouponService;
|
|
|
- @Mock private PosFoodMapper posFoodMapper;
|
|
|
- @Mock private FoodSpecsValueMapper foodSpecsValueMapper;
|
|
|
-
|
|
|
- @Spy
|
|
|
- private OrderPromotionHelper orderPromotionHelper = new OrderPromotionHelper();
|
|
|
-
|
|
|
- @InjectMocks
|
|
|
- private UserOrderController controller;
|
|
|
-
|
|
|
- private MockedStatic<MessageUtils> messages;
|
|
|
-
|
|
|
- @BeforeEach
|
|
|
- void setUp()
|
|
|
- {
|
|
|
- messages = org.mockito.Mockito.mockStatic(MessageUtils.class);
|
|
|
- messages.when(() -> MessageUtils.message(any(String.class)))
|
|
|
- .thenAnswer(invocation -> invocation.getArgument(0));
|
|
|
-
|
|
|
- // 真实 Helper 的依赖用 mock 反射注入(Spy 实例与控制器共享同一对象)
|
|
|
- inject(orderPromotionHelper, "promotionCalcService", promotionCalcService);
|
|
|
- inject(orderPromotionHelper, "posOrderPromotionMapper", posOrderPromotionMapper);
|
|
|
- inject(orderPromotionHelper, "promotionUserCouponService", promotionUserCouponService);
|
|
|
- inject(orderPromotionHelper, "posFoodMapper", posFoodMapper);
|
|
|
- inject(orderPromotionHelper, "foodSpecsValueMapper", foodSpecsValueMapper);
|
|
|
-
|
|
|
- lenient().when(orderParentService.count(any())).thenReturn(0L);
|
|
|
- lenient().when(orderParentService.save(any(OrderParent.class))).thenReturn(true);
|
|
|
- lenient().when(orderParentService.getOne(any())).thenReturn(new OrderParent());
|
|
|
- lenient().when(infoAddressService.getById(ADDRESS_ID)).thenReturn(address());
|
|
|
- lenient().when(posStoreService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(store()));
|
|
|
- lenient().when(operatingHoursService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(List.of(hours()));
|
|
|
- lenient().when(selfDeliveryService.isSelfDelivery(any(), any())).thenReturn(false);
|
|
|
- lenient().when(posOrderService.getOne(any())).thenReturn(null);
|
|
|
- lenient().when(posOrderService.save(any(PosOrder.class))).thenReturn(true);
|
|
|
- lenient().when(userFootprintService.getOne(any())).thenReturn(null);
|
|
|
- lenient().when(userFootprintService.saveOrUpdate(any())).thenReturn(true);
|
|
|
- lenient().when(infoUserService.getOne(any())).thenReturn(null);
|
|
|
- lenient().when(posFoodMapper.selectById(FOOD_ID)).thenReturn(food());
|
|
|
- lenient().when(foodDeliveryPromotionQuoteService.quote(any(), eq(USER_ID))).thenReturn(quote());
|
|
|
- }
|
|
|
-
|
|
|
- @AfterEach
|
|
|
- void closeMessages()
|
|
|
- {
|
|
|
- messages.close();
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void create_shouldPersistConsistentParentAndChildAmounts() throws Exception
|
|
|
- {
|
|
|
- AjaxResult result = controller.createOrder(token(), input(100, 30.0, 100L));
|
|
|
-
|
|
|
- assertEquals(200, result.get("code"));
|
|
|
- ArgumentCaptor<OrderParent> parentCaptor = ArgumentCaptor.forClass(OrderParent.class);
|
|
|
- verify(orderParentService).save(parentCaptor.capture());
|
|
|
- assertEquals(Long.valueOf(130L), parentCaptor.getValue().getTotalAmount());
|
|
|
- assertEquals(Long.valueOf(100L), parentCaptor.getValue().getActualPayAmount());
|
|
|
- assertEquals(Long.valueOf(30L), parentCaptor.getValue().getTotalDiscount());
|
|
|
-
|
|
|
- ArgumentCaptor<PosOrder> orderCaptor = ArgumentCaptor.forClass(PosOrder.class);
|
|
|
- verify(posOrderService).save(orderCaptor.capture());
|
|
|
- PosOrder order = orderCaptor.getValue();
|
|
|
- assertEquals(Integer.valueOf(100), order.getAmount());
|
|
|
- assertEquals(Double.valueOf(30.0), order.getFreight());
|
|
|
- assertEquals(Integer.valueOf(100), order.getFoodAmount());
|
|
|
- assertEquals(Integer.valueOf(2), order.getPromotionPricingVersion());
|
|
|
- assertEquals(0, new BigDecimal("30").compareTo(order.getMerchantDeliverySubsidy()));
|
|
|
- assertEquals(0, BigDecimal.ZERO.compareTo(order.getCustomerDeliveryFee()));
|
|
|
- // food JSON 由服务端按数据库商品生成,不信客户端 food
|
|
|
- JSONArray foods = JSON.parseArray(order.getFood());
|
|
|
- assertEquals(1, foods.size());
|
|
|
- assertEquals(50, foods.getJSONObject(0).getIntValue("price"));
|
|
|
- assertEquals(2, foods.getJSONObject(0).getIntValue("number"));
|
|
|
- // 商品促销汇总只含满减10,不含免运30
|
|
|
- assertEquals(Integer.valueOf(10), order.getMdSalesReduction());
|
|
|
-
|
|
|
- verify(posOrderPromotionMapper).insertBatch(any());
|
|
|
- verify(promotionUserCouponService, never()).markAsUsed(any(), any(), any());
|
|
|
- verify(deliveryOrderNotificationService).notifyOrderAvailable(any(PosOrder.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void create_shouldRejectChangedQuoteAndRollback()
|
|
|
- {
|
|
|
- ServiceException rejected = assertThrows(ServiceException.class,
|
|
|
- () -> controller.createOrder(token(), input(120, 30.0, 100L)));
|
|
|
- assertEquals("promotion.freeDelivery.quoteChanged", rejected.getMessage());
|
|
|
- verify(orderParentService, never()).save(any(OrderParent.class));
|
|
|
- verify(posOrderService, never()).save(any(PosOrder.class));
|
|
|
- verify(posOrderPromotionMapper, never()).insertBatch(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void create_shouldRejectMismatchedOrderTypesOrMultipleStoresInV2()
|
|
|
- {
|
|
|
- OrderCreateInput pickup = input(100, 30.0, 100L);
|
|
|
- pickup.setType(1L);
|
|
|
- ServiceException typeRejected = assertThrows(ServiceException.class,
|
|
|
- () -> controller.createOrder(token(), pickup));
|
|
|
- assertEquals("promotion.freeDelivery.unsupportedOrder", typeRejected.getMessage());
|
|
|
-
|
|
|
- OrderCreateInput multiStore = input(100, 30.0, 100L);
|
|
|
- OrderCreatItem second = versionTwoItem(100, 30.0);
|
|
|
- second.setMdId(2L);
|
|
|
- multiStore.setItems(new ArrayList<>(List.of(versionTwoItem(100, 30.0), second)));
|
|
|
- ServiceException multiRejected = assertThrows(ServiceException.class,
|
|
|
- () -> controller.createOrder(token(), multiStore));
|
|
|
- assertEquals("promotion.freeDelivery.unsupportedOrder", multiRejected.getMessage());
|
|
|
-
|
|
|
- verify(foodDeliveryPromotionQuoteService, never()).quote(any(), any());
|
|
|
- verify(posOrderService, never()).save(any(PosOrder.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void create_shouldNotUseZeroBenefitCoupon()
|
|
|
- {
|
|
|
- PromotionCalcResponse zeroBenefit = quote();
|
|
|
- zeroBenefit.setCouponId(100L);
|
|
|
- zeroBenefit.setAppliedCouponId(null);
|
|
|
- when(foodDeliveryPromotionQuoteService.quote(any(), eq(USER_ID))).thenReturn(zeroBenefit);
|
|
|
-
|
|
|
- controller.createOrder(token(), input(100, 30.0, 100L));
|
|
|
-
|
|
|
- verify(promotionUserCouponService, never()).markAsUsed(any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void legacyCreate_shouldKeepOriginalBehavior()
|
|
|
- {
|
|
|
- OrderCreateInput legacy = input(100, 30.0, 100L);
|
|
|
- legacy.getItems().get(0).setPromotionPricingVersion(null);
|
|
|
- legacy.getItems().get(0).setCartItems(null);
|
|
|
-
|
|
|
- controller.createOrder(token(), legacy);
|
|
|
-
|
|
|
- verify(foodDeliveryPromotionQuoteService, never()).quote(any(), any());
|
|
|
- verify(promotionCalcService, never()).calculate(any(), any());
|
|
|
- ArgumentCaptor<OrderParent> parentCaptor = ArgumentCaptor.forClass(OrderParent.class);
|
|
|
- verify(orderParentService).save(parentCaptor.capture());
|
|
|
- assertEquals(Long.valueOf(130L), parentCaptor.getValue().getTotalAmount());
|
|
|
- assertEquals(Long.valueOf(100L), parentCaptor.getValue().getActualPayAmount());
|
|
|
- assertNull(parentCaptor.getValue().getTotalDiscount());
|
|
|
- }
|
|
|
-
|
|
|
- // ==================== 夹具 ====================
|
|
|
-
|
|
|
- private static void assertNull(Object value)
|
|
|
- {
|
|
|
- assertTrue(value == null, "expected null but " + value);
|
|
|
- }
|
|
|
-
|
|
|
- private String token()
|
|
|
- {
|
|
|
- return JwtUtil.token(String.valueOf(USER_ID), "user");
|
|
|
- }
|
|
|
-
|
|
|
- private OrderCreateInput input(Integer childAmount, Double freight, Long actualPay)
|
|
|
- {
|
|
|
- OrderCreateInput input = new OrderCreateInput();
|
|
|
- input.setDdId(DD_ID);
|
|
|
- input.setShdzId(ADDRESS_ID);
|
|
|
- input.setType(0L);
|
|
|
- input.setTotalAmount(130L);
|
|
|
- input.setActualPayAmount(actualPay);
|
|
|
- input.setPaymentMethod("2");
|
|
|
- input.setItems(new ArrayList<>(List.of(versionTwoItem(childAmount, freight))));
|
|
|
- return input;
|
|
|
- }
|
|
|
-
|
|
|
- private OrderCreatItem versionTwoItem(Integer amount, Double freight)
|
|
|
- {
|
|
|
- OrderCreatItem item = new OrderCreatItem();
|
|
|
- item.setMdId(STORE_ID);
|
|
|
- item.setType(0L);
|
|
|
- item.setAmount(amount);
|
|
|
- item.setFreight(freight);
|
|
|
- item.setPromotionPricingVersion(2);
|
|
|
- PromotionCalcRequest.CartItem cartItem = new PromotionCalcRequest.CartItem();
|
|
|
- cartItem.setProductId(FOOD_ID);
|
|
|
- cartItem.setQuantity(2);
|
|
|
- item.setCartItems(new ArrayList<>(List.of(cartItem)));
|
|
|
- return item;
|
|
|
- }
|
|
|
-
|
|
|
- private InfoAddress address()
|
|
|
- {
|
|
|
- InfoAddress address = new InfoAddress();
|
|
|
- address.setId(ADDRESS_ID);
|
|
|
- address.setUserId(USER_ID);
|
|
|
- return address;
|
|
|
- }
|
|
|
-
|
|
|
- private PosStore store()
|
|
|
- {
|
|
|
- PosStore store = new PosStore();
|
|
|
- store.setId(STORE_ID.intValue());
|
|
|
- store.setUserId(5L);
|
|
|
- store.setState(0L);
|
|
|
- store.setType(1L);
|
|
|
- store.setLongitude(new BigDecimal("121.5654"));
|
|
|
- store.setLatitude(new BigDecimal("25.0330"));
|
|
|
- return store;
|
|
|
- }
|
|
|
-
|
|
|
- private OperatingHours hours()
|
|
|
- {
|
|
|
- OperatingHours hours = new OperatingHours();
|
|
|
- hours.setMdId(STORE_ID);
|
|
|
- hours.setDayOfWeek(LocalDate.now().getDayOfWeek().getValue());
|
|
|
- hours.setIsOpen(true);
|
|
|
- hours.setStartTime("00:00");
|
|
|
- hours.setEndTime("23:59");
|
|
|
- return hours;
|
|
|
- }
|
|
|
-
|
|
|
- private PosFood food()
|
|
|
- {
|
|
|
- PosFood food = new PosFood();
|
|
|
- food.setId(FOOD_ID);
|
|
|
- food.setMdid(STORE_ID);
|
|
|
- food.setPrice(new BigDecimal("50"));
|
|
|
- food.setName("测试商品");
|
|
|
- food.setStackingUp("0");
|
|
|
- food.setToExamine("0");
|
|
|
- return food;
|
|
|
- }
|
|
|
-
|
|
|
- /** 服务端报价:O100/G100/R30/F30/D0/A100,明细含免运30与满减10 */
|
|
|
- private PromotionCalcResponse quote()
|
|
|
- {
|
|
|
- PromotionCalcResponse calc = new PromotionCalcResponse();
|
|
|
- calc.setPricingVersion(2);
|
|
|
- calc.setOriginalAmount(new BigDecimal("100"));
|
|
|
- calc.setFinalAmount(new BigDecimal("100"));
|
|
|
- calc.setDeliveryFee(new BigDecimal("30"));
|
|
|
- calc.setDeliveryDiscountAmount(new BigDecimal("30"));
|
|
|
- calc.setCustomerDeliveryFee(BigDecimal.ZERO);
|
|
|
- calc.setPayableAmount(new BigDecimal("100"));
|
|
|
-
|
|
|
- List<PromotionDetail> details = new ArrayList<>();
|
|
|
- PromotionDetail freeDelivery = new PromotionDetail();
|
|
|
- freeDelivery.setType("promotion");
|
|
|
- freeDelivery.setSubType(5);
|
|
|
- freeDelivery.setName("原价满100免配送费");
|
|
|
- freeDelivery.setRefId(51L);
|
|
|
- freeDelivery.setReduce(new BigDecimal("30"));
|
|
|
- freeDelivery.setOriginalAmount(new BigDecimal("100"));
|
|
|
- freeDelivery.setDeliveryFee(new BigDecimal("30"));
|
|
|
- freeDelivery.setCustomerDeliveryFee(BigDecimal.ZERO);
|
|
|
- freeDelivery.setThreshold(new BigDecimal("100"));
|
|
|
- details.add(freeDelivery);
|
|
|
- PromotionDetail manjian = new PromotionDetail();
|
|
|
- manjian.setType("promotion");
|
|
|
- manjian.setSubType(1);
|
|
|
- manjian.setName("满100减10");
|
|
|
- manjian.setRefId(61L);
|
|
|
- manjian.setReduce(new BigDecimal("10"));
|
|
|
- details.add(manjian);
|
|
|
- calc.setDetails(details);
|
|
|
- return calc;
|
|
|
- }
|
|
|
-
|
|
|
- private static void inject(Object target, String fieldName, Object value)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- java.lang.reflect.Field field = findField(target.getClass(), fieldName);
|
|
|
- field.setAccessible(true);
|
|
|
- field.set(target, value);
|
|
|
- }
|
|
|
- catch (Exception e)
|
|
|
- {
|
|
|
- throw new IllegalStateException("inject " + fieldName, e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private static java.lang.reflect.Field findField(Class<?> type, String fieldName) throws NoSuchFieldException
|
|
|
- {
|
|
|
- for (Class<?> current = type; current != null; current = current.getSuperclass())
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- return current.getDeclaredField(fieldName);
|
|
|
- }
|
|
|
- catch (NoSuchFieldException ignored)
|
|
|
- {
|
|
|
- // 继续向父类查找
|
|
|
- }
|
|
|
- }
|
|
|
- throw new NoSuchFieldException(fieldName);
|
|
|
- }
|
|
|
-}
|