|
|
@@ -0,0 +1,142 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.Coupon;
|
|
|
+import com.etotem.cfc.entity.CouponGrantLog;
|
|
|
+import com.etotem.cfc.entity.UserCoupon;
|
|
|
+import com.etotem.cfc.mapper.CouponGrantLogMapper;
|
|
|
+import com.etotem.cfc.mapper.CouponMapper;
|
|
|
+import com.etotem.cfc.mapper.UserCouponMapper;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * CouponService 单元测试(无 SpringBootTest,避免 MySQL 连接)
|
|
|
+ * - apply(): FIXED满减/CASH无门槛/DISCOUNT折扣 三类型核销
|
|
|
+ * - grant(): 幂等发券(period 防重用 + quantity 批量)
|
|
|
+ */
|
|
|
+public class CouponServiceTest {
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private CouponMapper couponMapper;
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private UserCouponMapper userCouponMapper;
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private CouponGrantLogMapper couponGrantLogMapper;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private CouponService couponService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ public void setup() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserCoupon userCoupon(Long id, Long userId, Long couponId) {
|
|
|
+ UserCoupon uc = new UserCoupon();
|
|
|
+ uc.setId(id);
|
|
|
+ uc.setUserId(userId);
|
|
|
+ uc.setCouponId(couponId);
|
|
|
+ uc.setStatus("AVAILABLE");
|
|
|
+ return uc;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Coupon coupon(String type, Integer value, Integer minSpend, Integer discountRate) {
|
|
|
+ Coupon c = new Coupon();
|
|
|
+ c.setType(type);
|
|
|
+ c.setValue(value);
|
|
|
+ c.setMinSpend(minSpend);
|
|
|
+ c.setDiscountRate(discountRate);
|
|
|
+ c.setValidFrom(new Date(System.currentTimeMillis() - 86400000L));
|
|
|
+ c.setValidUntil(new Date(System.currentTimeMillis() + 86400000L));
|
|
|
+ c.setApplicableTo("ALL");
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyFixedEnforcesMinSpend() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("FIXED", 1000, 2000, null));
|
|
|
+ // 订单金额1000分 < 起用门槛2000分 -> 不可用
|
|
|
+ assertNull(couponService.apply(100L, 1L, "ALL", 1000));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyFixedWorksAboveMinSpend() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("FIXED", 1000, 2000, null));
|
|
|
+ // 满减券抵扣金额 = value,不超订单
|
|
|
+ assertEquals(Integer.valueOf(1000), couponService.apply(100L, 1L, "ALL", 3000));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyFixedCappedByOrderAmount() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("FIXED", 500, 0, null));
|
|
|
+ // 满减券抵扣不能超过订单金额
|
|
|
+ assertEquals(Integer.valueOf(300), couponService.apply(100L, 1L, "ALL", 300));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyCashIgnoresMinSpend() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("CASH", 500, 1000, null));
|
|
|
+ // 无门槛券(CASH)忽略min_spend:订单300 < 配置门槛1000 仍可用,抵扣300(不超订单)
|
|
|
+ assertEquals(Integer.valueOf(300), couponService.apply(100L, 1L, "ALL", 300));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyDiscountComputesRate() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("DISCOUNT", 0, 0, 9000));
|
|
|
+ // 订单3000分 9折(9000) -> 抵扣 3000*(10000-9000)/10000=300,实付2700
|
|
|
+ assertEquals(Integer.valueOf(300), couponService.apply(100L, 1L, "ALL", 3000));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void applyDiscountSmallOrder() {
|
|
|
+ when(userCouponMapper.selectById(1L)).thenReturn(userCoupon(1L, 100L, 1L));
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("DISCOUNT", 0, 0, 9000));
|
|
|
+ // 小订单100分 9折 -> 抵扣10,实付90
|
|
|
+ assertEquals(Integer.valueOf(10), couponService.apply(100L, 1L, "ALL", 100));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void grantSkipsExistingPeriod() {
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(coupon("FIXED", 1000, 0, null));
|
|
|
+ when(couponGrantLogMapper.selectCount(any())).thenReturn(1L);
|
|
|
+ // PERIODIC 同周期已发过 -> 跳过,不发券
|
|
|
+ boolean granted = couponService.grant(100L, 1L, "PERIODIC", "2026-08", null);
|
|
|
+ assertFalse(granted);
|
|
|
+ verify(userCouponMapper, never()).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void grantIssuesQuantityAndLogs() {
|
|
|
+ Coupon c = coupon("FIXED", 1000, 0, null);
|
|
|
+ c.setGrantQuantity(4);
|
|
|
+ when(couponMapper.selectById(1L)).thenReturn(c);
|
|
|
+ when(couponGrantLogMapper.selectCount(any())).thenReturn(0L);
|
|
|
+ // JOIN 发放:按 grant_quantity=4 批量发券 + 落1条流水
|
|
|
+ boolean granted = couponService.grant(100L, 1L, "JOIN", null, "ORD123");
|
|
|
+ assertTrue(granted);
|
|
|
+ verify(userCouponMapper, times(4)).insert(any());
|
|
|
+ verify(couponGrantLogMapper, times(1)).insert(any(CouponGrantLog.class));
|
|
|
+ }
|
|
|
+}
|