|
|
@@ -0,0 +1,205 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.Cart;
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
+import com.etotem.cfc.entity.ProductSku;
|
|
|
+import com.etotem.cfc.mapper.CartMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductMapper;
|
|
|
+import com.etotem.cfc.pricing.ComboChild;
|
|
|
+import com.etotem.cfc.pricing.ComboOfferDef;
|
|
|
+import com.etotem.cfc.pricing.ComboOfferResult;
|
|
|
+import com.etotem.cfc.pricing.LineItem;
|
|
|
+import com.etotem.cfc.service.CartService;
|
|
|
+import com.etotem.cfc.service.ComboOfferService;
|
|
|
+import com.etotem.cfc.service.MemberPriceResolver;
|
|
|
+import com.etotem.cfc.service.ProductGiftRuleService;
|
|
|
+import com.etotem.cfc.service.ProductSkuService;
|
|
|
+import com.etotem.cfc.service.SysConfigService;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+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.anyList;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.Mockito.doThrow;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * CartService.computeComboOffer / replaceCartWithCombo 服务层单测(mock mapper,Review Focus 4/5)。
|
|
|
+ * 勾选态是前端本地态(CartItemDTO.id=购物车主键),后端按勾选子集重算。
|
|
|
+ */
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class CartReplaceComboTest {
|
|
|
+
|
|
|
+ @Mock private CartMapper cartMapper;
|
|
|
+ @Mock private ProductMapper productMapper;
|
|
|
+ @Mock private ProductSkuService productSkuService;
|
|
|
+ @Mock private ProductGiftRuleService productGiftRuleService;
|
|
|
+ @Mock private SysConfigService sysConfigService;
|
|
|
+ @Mock private MemberPriceResolver memberPriceResolver;
|
|
|
+ @Mock private ComboOfferService comboOfferService;
|
|
|
+
|
|
|
+ @InjectMocks private CartService cartService;
|
|
|
+
|
|
|
+ private Product product(Long id, String name, int price) {
|
|
|
+ Product p = new Product();
|
|
|
+ p.setId(id);
|
|
|
+ p.setName(name);
|
|
|
+ p.setPrice(price);
|
|
|
+ p.setProductType("physical");
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ProductSku sku(Long id, Long productId, int price, Long linkedProductId) {
|
|
|
+ ProductSku s = new ProductSku();
|
|
|
+ s.setId(id);
|
|
|
+ s.setProductId(productId);
|
|
|
+ s.setPrice(price);
|
|
|
+ s.setLinkedProductId(linkedProductId);
|
|
|
+ return s;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Cart cart(Long id, Long productId, Long skuId, Integer qty) {
|
|
|
+ Cart c = new Cart();
|
|
|
+ c.setId(id);
|
|
|
+ c.setUserId(100L);
|
|
|
+ c.setProductId(productId);
|
|
|
+ c.setSkuId(skuId);
|
|
|
+ c.setQuantity(qty);
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ComboOfferDef defAB() {
|
|
|
+ ComboOfferDef d = new ComboOfferDef();
|
|
|
+ d.setComboSkuId(900L);
|
|
|
+ d.setComboName("套餐X");
|
|
|
+ d.setComboPrice(15000);
|
|
|
+ ComboChild a = new ComboChild();
|
|
|
+ a.setChildProductId(1L);
|
|
|
+ a.setChildSkuId(11L);
|
|
|
+ a.setQuantity(2);
|
|
|
+ ComboChild b = new ComboChild();
|
|
|
+ b.setChildProductId(2L);
|
|
|
+ b.setChildSkuId(22L);
|
|
|
+ b.setQuantity(1);
|
|
|
+ d.setChildren(Arrays.asList(a, b));
|
|
|
+ return d;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 仅按勾选记录计算(未勾选行不参与),saving>0 才 active */
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ void computeComboOffer_onlyCheckedRowsInvolved() {
|
|
|
+ Cart ca = cart(1L, 1L, 11L, 2);
|
|
|
+ Cart cb = cart(2L, 2L, 22L, 1);
|
|
|
+ when(cartMapper.selectList(any())).thenReturn(Arrays.asList(ca, cb));
|
|
|
+ when(productMapper.selectById(1L)).thenReturn(product(1L, "A", 5000));
|
|
|
+ when(productMapper.selectById(2L)).thenReturn(product(2L, "B", 6000));
|
|
|
+ when(productSkuService.getById(11L)).thenReturn(sku(11L, 1L, 5000, null));
|
|
|
+ when(productSkuService.getById(22L)).thenReturn(sku(22L, 2L, 6000, null));
|
|
|
+ when(memberPriceResolver.resolveEligibleMemberPrice(any(), any())).thenReturn(0);
|
|
|
+ ComboOfferResult stub = new ComboOfferResult();
|
|
|
+ stub.setActive(true);
|
|
|
+ when(comboOfferService.computeForLines(anyList())).thenReturn(stub);
|
|
|
+
|
|
|
+ ComboOfferResult r = cartService.computeComboOffer(100L, Arrays.asList(1L, 2L));
|
|
|
+
|
|
|
+ assertTrue(r.isActive());
|
|
|
+ ArgumentCaptor<List<LineItem>> captor = ArgumentCaptor.forClass((Class) List.class);
|
|
|
+ verify(comboOfferService).computeForLines(captor.capture());
|
|
|
+ assertEquals(2, captor.getValue().size()); // 仅勾选 2 行参与
|
|
|
+ LineItem l0 = captor.getValue().get(0);
|
|
|
+ assertEquals(1L, l0.getProductId());
|
|
|
+ assertEquals(11L, l0.getSkuId());
|
|
|
+ assertEquals(2, l0.getQuantity());
|
|
|
+ assertEquals(5000, l0.getUnitPrice());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 后端重算组数(忽略前端传入):A×4+B×2 传 99 份 → 只替换 2 组 */
|
|
|
+ @Test
|
|
|
+ void replaceCartWithCombo_recomputesGroupsIgnoringFrontend() {
|
|
|
+ Cart ca = cart(1L, 1L, 11L, 4);
|
|
|
+ Cart cb = cart(2L, 2L, 22L, 2);
|
|
|
+ when(cartMapper.selectList(any())).thenReturn(Arrays.asList(ca, cb));
|
|
|
+ when(comboOfferService.loadDef(900L)).thenReturn(defAB());
|
|
|
+ when(productSkuService.getById(900L)).thenReturn(sku(900L, 99L, 15000, null));
|
|
|
+ when(cartMapper.selectOne(any())).thenReturn(null); // 无既有套餐行
|
|
|
+
|
|
|
+ Result<Void> res = cartService.replaceCartWithCombo(100L, Arrays.asList(1L, 2L), 900L);
|
|
|
+
|
|
|
+ assertNotNull(res);
|
|
|
+ // 子商品行扣 2 组:A 扣4→0 删、B 扣2→0 删
|
|
|
+ verify(cartMapper).deleteById(1L);
|
|
|
+ verify(cartMapper).deleteById(2L);
|
|
|
+ // 新增套餐 SKU 行 quantity=2(后端重算,非前端 99)
|
|
|
+ ArgumentCaptor<Cart> captor = ArgumentCaptor.forClass(Cart.class);
|
|
|
+ verify(cartMapper).insert(captor.capture());
|
|
|
+ assertEquals(99L, captor.getValue().getProductId());
|
|
|
+ assertEquals(900L, captor.getValue().getSkuId());
|
|
|
+ assertEquals(2, captor.getValue().getQuantity());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 余量保留:A×5+B×2 → A 余 1、B 余 0、套餐行 2 */
|
|
|
+ @Test
|
|
|
+ void replaceCartWithCombo_keepsRemainder() {
|
|
|
+ Cart ca = cart(1L, 1L, 11L, 5);
|
|
|
+ Cart cb = cart(2L, 2L, 22L, 2);
|
|
|
+ when(cartMapper.selectList(any())).thenReturn(Arrays.asList(ca, cb));
|
|
|
+ when(comboOfferService.loadDef(900L)).thenReturn(defAB());
|
|
|
+ when(productSkuService.getById(900L)).thenReturn(sku(900L, 99L, 15000, null));
|
|
|
+ when(cartMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ cartService.replaceCartWithCombo(100L, Arrays.asList(1L, 2L), 900L);
|
|
|
+
|
|
|
+ verify(cartMapper).updateById(ca); // A 扣 4 → 余 1
|
|
|
+ assertEquals(1, ca.getQuantity());
|
|
|
+ verify(cartMapper).deleteById(2L); // B 扣 2 → 0 删
|
|
|
+ verify(cartMapper).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 套餐商品已在购物车(同 product+sku)→ 合并数量而非重复套装 */
|
|
|
+ @Test
|
|
|
+ void replaceCartWithCombo_comboAlreadyInCart_mergesQuantity() {
|
|
|
+ Cart ca = cart(1L, 1L, 11L, 4);
|
|
|
+ Cart cb = cart(2L, 2L, 22L, 2);
|
|
|
+ Cart existingCombo = cart(3L, 99L, 900L, 1);
|
|
|
+ when(cartMapper.selectList(any())).thenReturn(Arrays.asList(ca, cb));
|
|
|
+ when(comboOfferService.loadDef(900L)).thenReturn(defAB());
|
|
|
+ when(productSkuService.getById(900L)).thenReturn(sku(900L, 99L, 15000, null));
|
|
|
+ when(cartMapper.selectOne(any())).thenReturn(existingCombo);
|
|
|
+
|
|
|
+ cartService.replaceCartWithCombo(100L, Arrays.asList(1L, 2L), 900L);
|
|
|
+
|
|
|
+ verify(cartMapper, never()).insert(any());
|
|
|
+ verify(cartMapper).updateById(existingCombo);
|
|
|
+ assertEquals(3, existingCombo.getQuantity()); // 1 + 2
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 事务性:任一步失败(deleteById 抛异常)→ 传播且未插入套餐行(整体回滚语义) */
|
|
|
+ @Test
|
|
|
+ void replace_anyStepFails_throwsAndNoComboInsert() {
|
|
|
+ Cart ca = cart(1L, 1L, 11L, 4);
|
|
|
+ Cart cb = cart(2L, 2L, 22L, 2);
|
|
|
+ when(cartMapper.selectList(any())).thenReturn(Arrays.asList(ca, cb));
|
|
|
+ when(comboOfferService.loadDef(900L)).thenReturn(defAB());
|
|
|
+ doThrow(new RuntimeException("boom")).when(cartMapper).deleteById(anyLong());
|
|
|
+
|
|
|
+ assertThrows(RuntimeException.class,
|
|
|
+ () -> cartService.replaceCartWithCombo(100L, Arrays.asList(1L, 2L), 900L));
|
|
|
+ verify(cartMapper, never()).insert(any());
|
|
|
+ }
|
|
|
+}
|