|
@@ -0,0 +1,381 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.entity.Coupon;
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyCoupon;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.CouponMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyCouponMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.CouponService;
|
|
|
|
|
+import com.etotem.cfc.service.MembershipService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测:优惠券下单抵扣(能力键 {@code shop.coupon.apply},S-cfc-167)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>契约溯源:</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-P-57 优惠券使用(业务域 F 商城与会员)——
|
|
|
|
|
+ * 主流程:选券 → 结算抵扣;边界:券过期 / 不满足门槛不可用。</li>
|
|
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.32 优惠券(家庭维度)
|
|
|
|
|
+ * {@code POST /api/coupon/apply} 下单抵扣(家庭券)。</li>
|
|
|
|
|
+ * <li>实现承载:{@code CouponController.apply} → {@link CouponService#apply(Long, Long, String, Integer,
|
|
|
|
|
+ * java.util.Collection)}({@code orderType=PRODUCT} 结算预览 + {@code ProductOrderService} 下单复用)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>覆盖流</b>(纯 Mockito,无 Spring 上下文 / 无 DB / 无外部服务):</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li><b>正常流</b>:满减券(FIXED/按面额)按面额抵扣(面额不超过订单金额)、折扣券按折扣率抵扣(含四舍五入)、
|
|
|
|
|
+ * 全场通用券任意订单类型可用、商品绑定券订单含该商品可用、抵扣金额不超过订单金额(面额封顶);</li>
|
|
|
|
|
+ * <li><b>异常/边界流</b>:券未开始 / 已过期 / 已停用不可用、满减券(FIXED)门槛不足不可用(CASH 无门槛券忽略门槛)、
|
|
|
|
|
+ * applicableTo 与订单类型不匹配不可用、商品绑定券订单不含该商品不可用、会员专属券非会员不可用;
|
|
|
|
|
+ * 用户未加入家庭 / 券实例不存在 / 券实例非 AVAILABLE 同样不可用;</li>
|
|
|
|
|
+ * <li><b>权限流</b>:他人家庭的券实例不可用(family_id 不匹配 → 不抵扣)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ */
|
|
|
|
|
+class ShopCouponApplyContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private CouponService service;
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+ private CouponMapper couponMapper;
|
|
|
|
|
+ private FamilyCouponMapper familyCouponMapper;
|
|
|
|
|
+ private MembershipService membershipService;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ service = new CouponService();
|
|
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
|
|
+ couponMapper = mock(CouponMapper.class);
|
|
|
|
|
+ familyCouponMapper = mock(FamilyCouponMapper.class);
|
|
|
|
|
+ membershipService = mock(MembershipService.class);
|
|
|
|
|
+
|
|
|
|
|
+ inject(service, "userMapper", userMapper);
|
|
|
|
|
+ inject(service, "couponMapper", couponMapper);
|
|
|
|
|
+ inject(service, "familyCouponMapper", familyCouponMapper);
|
|
|
|
|
+ inject(service, "membershipService", membershipService);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void inject(Object target, String field, Object value) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(field);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 用户属于家庭 5,券实例/券模板均属于该家庭且可用 */
|
|
|
|
|
+ private void stubFamilyAndCoupon(FamilyCoupon fc, Coupon coupon) {
|
|
|
|
|
+ User user = new User();
|
|
|
|
|
+ user.setId(1L);
|
|
|
|
|
+ user.setFamilyId(5L);
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
|
|
+ when(familyCouponMapper.selectById(fc.getId())).thenReturn(fc);
|
|
|
|
|
+ when(couponMapper.selectById(coupon.getId())).thenReturn(coupon);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 仅注入「用户在家庭 5」的上下文,不预置 mapper 查询结果(由调用方按需 stub) */
|
|
|
|
|
+ private void stubUserSameFamily() {
|
|
|
|
|
+ User user = new User();
|
|
|
|
|
+ user.setId(1L);
|
|
|
|
|
+ user.setFamilyId(5L);
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 未加入家庭的用户(family_id 为空) */
|
|
|
|
|
+ private void stubUserNoFamily() {
|
|
|
|
|
+ User user = new User();
|
|
|
|
|
+ user.setId(1L);
|
|
|
|
|
+ user.setFamilyId(null);
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyCoupon availableFamilyCoupon(Long id, Long familyId, Long couponId) {
|
|
|
|
|
+ FamilyCoupon fc = new FamilyCoupon();
|
|
|
|
|
+ fc.setId(id);
|
|
|
|
|
+ fc.setFamilyId(familyId);
|
|
|
|
|
+ fc.setCouponId(couponId);
|
|
|
|
|
+ fc.setStatus("AVAILABLE");
|
|
|
|
|
+ return fc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Coupon activeCoupon(String type) {
|
|
|
|
|
+ Coupon c = new Coupon();
|
|
|
|
|
+ c.setId(100L);
|
|
|
|
|
+ c.setName("测试券");
|
|
|
|
|
+ c.setType(type);
|
|
|
|
|
+ c.setStatus("ACTIVE");
|
|
|
|
|
+ c.setApplicableTo("ALL");
|
|
|
|
|
+ c.setValue(1000); // 10 元
|
|
|
|
|
+ c.setMinSpend(0);
|
|
|
|
|
+ c.setDiscountRate(9000); // 9 折
|
|
|
|
|
+ c.setValidFrom(new Date(System.currentTimeMillis() - 86400000L * 7)); // 7 天前开始
|
|
|
|
|
+ c.setValidUntil(new Date(System.currentTimeMillis() + 86400000L * 7)); // 7 天后到期
|
|
|
|
|
+ return c;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常流(选券 → 结算抵扣) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-1:满减券按面额抵扣(面额不超过订单金额)")
|
|
|
|
|
+ void 正常流_满减券按面额抵扣() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(500L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH"); // value=1000
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 500L, "PRODUCT", 9900, Collections.singleton(10L));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount, "满 0 元减 10 元券,订单 99 元应抵扣 10 元");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-2:折扣券按折扣率抵扣(含四舍五入)")
|
|
|
|
|
+ void 正常流_折扣券按折扣率抵扣() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(501L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("DISCOUNT");
|
|
|
|
|
+ coupon.setDiscountRate(8000); // 8 折 → 抵 20%
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 501L, "PRODUCT", 9900, Collections.singleton(10L));
|
|
|
|
|
+
|
|
|
|
|
+ // 9900 * (10000-8000)/10000 = 1980
|
|
|
|
|
+ assertEquals(Integer.valueOf(1980), discount, "99 元订单 8 折券应抵扣 19.8 元");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-3:全场通用券(applicableTo=ALL)可用于任意订单类型")
|
|
|
|
|
+ void 正常流_全场通用券任意订单类型可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(502L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setApplicableTo("ALL");
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 502L, "MEMBERSHIP", 19800, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount, "全场通用券可用于 MEMBERSHIP 订单");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-4:商品绑定券,订单包含该商品时可用")
|
|
|
|
|
+ void 正常流_商品绑定券订单含该商品可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(503L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setProductId(3L); // 绑定商品 3(如菌群测评券)
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 503L, "PRODUCT", 9900, Collections.singleton(3L));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount, "订单包含绑定商品时商品券可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-5:抵扣金额不超过订单金额(面额封顶)")
|
|
|
|
|
+ void 正常流_抵扣不超过订单金额() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(504L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH"); // value=1000,订单仅 5 元
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 504L, "PRODUCT", 500, Collections.singleton(10L));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(500), discount, "面额 10 元 > 订单 5 元,抵扣应为订单金额 5 元,不能超单");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 异常/边界流(券过期/不满足门槛不可用) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-1:未到开始时间不可用")
|
|
|
|
|
+ void 边界流_未开始不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(505L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setValidFrom(new Date(System.currentTimeMillis() + 86400000L)); // 明天开始
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 505L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "未到 validFrom 的券不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-2:已过期不可用")
|
|
|
|
|
+ void 边界流_已过期不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(506L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setValidUntil(new Date(System.currentTimeMillis() - 86400000L)); // 昨天到期
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 506L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "validUntil 已过的券不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-3:已停用模板不可用")
|
|
|
|
|
+ void 边界流_已停用不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(507L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setStatus("DISABLED");
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 507L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "status 非 ACTIVE 的券模板不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-4:满减门槛不足不可用(FIXED/非 CASH 类型券校验门槛)")
|
|
|
|
|
+ void 边界流_满减门槛不足不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(508L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setType("FIXED"); // 满减券:需校验门槛
|
|
|
|
|
+ coupon.setMinSpend(10000); // 门槛 100 元,订单 99 元
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 508L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "未达最低消费门槛的满减券不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-4b:满减门槛满足时可用(FIXED 满减券按面额抵扣)")
|
|
|
|
|
+ void 边界流_满减门槛满足可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(509L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setType("FIXED"); // 满减券
|
|
|
|
|
+ coupon.setMinSpend(10000); // 门槛 100 元
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 509L, "PRODUCT", 19900, Collections.singleton(10L));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount,
|
|
|
|
|
+ "订单 199 元满足满 100 减 10 门槛,应抵扣面额 10 元");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-5:CASH 无门槛券忽略门槛设置")
|
|
|
|
|
+ void 边界流_cash券忽略门槛() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(510L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setType("CASH");
|
|
|
|
|
+ // minSpend 已由 activeCoupon 设为 0 —— 即使门槛 > 0,CASH 券也不校验
|
|
|
|
|
+ coupon.setMinSpend(999999);
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 510L, "PRODUCT", 9900, Collections.singleton(10L));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount, "CASH 无门槛券忽略起用门槛");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-6:applicableTo 与订单类型不匹配不可用")
|
|
|
|
|
+ void 边界流_适用范围不匹配不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(511L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setApplicableTo("MEMBERSHIP"); // 仅限会员订单
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 511L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "applicableTo=MEMBERSHIP 的券不可用于 PRODUCT 订单");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-7:商品绑定券,订单不含该商品不可用")
|
|
|
|
|
+ void 边界流_商品绑定券订单不含商品不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(512L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setProductId(3L);
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 512L, "PRODUCT", 9900, Collections.singleton(9L)),
|
|
|
|
|
+ "商品券用于非绑定商品订单不可用(product_id=3 而订单商品=9)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-8:商品绑定券,无商品上下文(会员订单)不可用")
|
|
|
|
|
+ void 边界流_商品绑定券无商品上下文不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(513L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setProductId(3L);
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 513L, "MEMBERSHIP", 9900, null),
|
|
|
|
|
+ "无 productIds 上下文时绑定商品的券一律不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-9:会员专属券(非 MANUAL/非转赠)非会员不可用")
|
|
|
|
|
+ void 边界流_会员专属券非会员不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(514L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setApplicableTo("MEMBERSHIP");
|
|
|
|
|
+ coupon.setGrantType(null);
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+ when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 514L, "MEMBERSHIP", 19800, null),
|
|
|
|
|
+ "会员专属券非会员不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-10:会员专属券会员可用(grantType=MANUAL 豁免会员校验也可用)")
|
|
|
|
|
+ void 边界流_会员专属券手动发放无门槛() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(515L, 5L, 100L);
|
|
|
|
|
+ Coupon coupon = activeCoupon("CASH");
|
|
|
|
|
+ coupon.setApplicableTo("MEMBERSHIP");
|
|
|
|
|
+ coupon.setGrantType("MANUAL"); // 管理员手动发放,豁免会员校验
|
|
|
|
|
+ stubFamilyAndCoupon(fc, coupon);
|
|
|
|
|
+ when(membershipService.getMemberLevel(1L)).thenReturn("FREE");
|
|
|
|
|
+
|
|
|
|
|
+ Integer discount = service.apply(1L, 515L, "MEMBERSHIP", 19800, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(1000), discount,
|
|
|
|
|
+ "grantType=MANUAL 的会员券(管理员送券)非会员也可使用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 权限流 / 前置校验 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流-1:他人家庭的券实例不可用")
|
|
|
|
|
+ void 权限流_他人家庭券不可用() {
|
|
|
|
|
+ stubFamilyAndCoupon(availableFamilyCoupon(516L, 99L, 100L), activeCoupon("CASH"));
|
|
|
|
|
+ // 用户家庭=5,券实例 familyId=99
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 516L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "他人家庭的券不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("前置-1:未加入家庭的用户不可用券")
|
|
|
|
|
+ void 前置_未加入家庭不可用() {
|
|
|
|
|
+ stubUserNoFamily();
|
|
|
|
|
+ when(familyCouponMapper.selectById(517L)).thenReturn(availableFamilyCoupon(517L, 5L, 100L));
|
|
|
|
|
+ when(couponMapper.selectById(100L)).thenReturn(activeCoupon("CASH"));
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 517L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "未加入家庭(family_id 为空)的用户不能用家庭券");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("前置-2:券实例不存在不可用")
|
|
|
|
|
+ void 前置_券实例不存在不可用() {
|
|
|
|
|
+ stubUserSameFamily();
|
|
|
|
|
+ when(familyCouponMapper.selectById(518L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 518L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "券实例不存在不可用");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("前置-3:券实例非 AVAILABLE 状态(已使用)不可用")
|
|
|
|
|
+ void 前置_券实例非可用状态不可用() {
|
|
|
|
|
+ FamilyCoupon fc = availableFamilyCoupon(519L, 5L, 100L);
|
|
|
|
|
+ fc.setStatus("USED");
|
|
|
|
|
+ stubFamilyAndCoupon(fc, activeCoupon("CASH"));
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(service.apply(1L, 519L, "PRODUCT", 9900, Collections.singleton(10L)),
|
|
|
|
|
+ "USED 状态的券实例不可重复抵扣");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|