|
|
@@ -1,37 +1,70 @@
|
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
-import com.etotem.cfc.entity.CfRateTier;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.etotem.cfc.config.ConfigService;
|
|
|
+import com.etotem.cfc.constant.CfConfigKeys;
|
|
|
+import com.etotem.cfc.dto.CfEarnResult;
|
|
|
+import com.etotem.cfc.entity.CfDirectSalesTier;
|
|
|
+import com.etotem.cfc.entity.CfEarnRule;
|
|
|
+import com.etotem.cfc.entity.CfIndirectSalesTier;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.OctopusEffectRecord;
|
|
|
+import com.etotem.cfc.entity.OctopusKeyPerson;
|
|
|
+import com.etotem.cfc.entity.PpointConsumptionLog;
|
|
|
import com.etotem.cfc.entity.Product;
|
|
|
import com.etotem.cfc.entity.ReferralTree;
|
|
|
import com.etotem.cfc.entity.User;
|
|
|
import com.etotem.cfc.mapper.CfTransferRecordMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusEffectKeyPersonMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusEffectRecordMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusKeyPersonMapper;
|
|
|
+import com.etotem.cfc.mapper.PpointConsumptionLogMapper;
|
|
|
import com.etotem.cfc.mapper.ProductMapper;
|
|
|
import com.etotem.cfc.mapper.ReferralTreeMapper;
|
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
import org.mockito.InjectMocks;
|
|
|
import org.mockito.Mock;
|
|
|
import org.mockito.MockitoAnnotations;
|
|
|
+import org.mockito.Spy;
|
|
|
|
|
|
import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Map;
|
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
|
-import static org.mockito.ArgumentMatchers.anyInt;
|
|
|
import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
|
import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
import static org.mockito.Mockito.verify;
|
|
|
import static org.mockito.Mockito.when;
|
|
|
|
|
|
/**
|
|
|
- * CF 分佣"只返第一个非同家庭引荐人"(设计决策 D5)结算行为测试。
|
|
|
+ * CF 分佣收口(F-4)两级销售奖结算行为测试。
|
|
|
*
|
|
|
- * 验证 settle 在推荐链上的上溯语义:
|
|
|
- * - 同家庭推荐人跳过,继续上溯
|
|
|
- * - 找到第一个非同家庭引荐人后发放分润并终止(不再给更远推荐人发放)
|
|
|
+ * <p>覆盖口径(计划 §四 / §3.1 / §4.2 / §4.4):</p>
|
|
|
+ * <ul>
|
|
|
+ * <li>第 1 个非同家庭引荐人 → F03a 直接销售奖,比例取"团队 8 层内滚动一年累计 P 点"档位;</li>
|
|
|
+ * <li>第 2 个非同家庭引荐人 → F03b 间接销售奖,比例取"自身滚动一年累计消费 P 点"档位;</li>
|
|
|
+ * <li>共同基数 = 本单消费产生的 P 点(商品按有效 P 点比例折算);</li>
|
|
|
+ * <li>同家庭互推(D5)跳过并顺延,最多两级;</li>
|
|
|
+ * <li>档位比例超 {@code cf_earn_rule.params_json.max_rate_percent} 时按上限封顶;</li>
|
|
|
+ * <li>发放统一走 F-2 的 {@code earnByRule}(幂等键复用 F-2 唯一键);销售奖不再写第二套流水
|
|
|
+ * ({@code cf_transfer_record} 仅买家 F02 路径写);</li>
|
|
|
+ * <li>F03a 发放时创建感恩记录,幂等命中(duplicated)不重复创建。</li>
|
|
|
+ * </ul>
|
|
|
*/
|
|
|
class CfCommissionServiceSettleTest {
|
|
|
|
|
|
@@ -51,6 +84,24 @@ class CfCommissionServiceSettleTest {
|
|
|
private SysConfigService sysConfigService;
|
|
|
@Mock
|
|
|
private ProductMapper productMapper;
|
|
|
+ @Mock
|
|
|
+ private OctopusKeyPersonMapper octopusKeyPersonMapper;
|
|
|
+ @Mock
|
|
|
+ private OctopusEffectRecordMapper octopusEffectRecordMapper;
|
|
|
+ @Mock
|
|
|
+ private OctopusEffectKeyPersonMapper octopusEffectKeyPersonMapper;
|
|
|
+ @Mock
|
|
|
+ private CfBalanceService cfBalanceService;
|
|
|
+ @Mock
|
|
|
+ private CfEarnRuleService cfEarnRuleService;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private PpointConsumptionLogMapper ppointConsumptionLogMapper;
|
|
|
+ @Mock
|
|
|
+ private ConfigService configService;
|
|
|
+ @Spy
|
|
|
+ private ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
|
@InjectMocks
|
|
|
private CfCommissionService cfCommissionService;
|
|
|
@@ -60,19 +111,70 @@ class CfCommissionServiceSettleTest {
|
|
|
private static final Long BUYER_ID = 100L;
|
|
|
private static final Long BUYER_FAMILY = 100L;
|
|
|
|
|
|
+ /** 家庭成员ID = 用户ID + 1000,便于断言 */
|
|
|
+ private static Long memberIdOf(Long userId) {
|
|
|
+ return userId + 1000;
|
|
|
+ }
|
|
|
+
|
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
|
MockitoAnnotations.openMocks(this);
|
|
|
- // 商品:P点=100,平台分享比例=1000bps(10%)
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), ReferralTree.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyMember.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), OctopusKeyPerson.class);
|
|
|
+
|
|
|
+ // 商品:有效 P 点=100(商品订单 100 元 → 基数 100 P 点)
|
|
|
Product p = new Product();
|
|
|
p.setId(PRODUCT_ID);
|
|
|
p.setCategoryId(1L);
|
|
|
when(productMapper.selectById(PRODUCT_ID)).thenReturn(p);
|
|
|
when(ppointConfigService.getEffectivePpoint(PRODUCT_ID, 1L)).thenReturn(100);
|
|
|
+ // 买家 F02 路径(本期不改口径)
|
|
|
when(sysConfigService.getValue("product_platform_points_share")).thenReturn("1000");
|
|
|
when(cfTransferRecordMapper.insert(any())).thenReturn(1);
|
|
|
+ // 非商品路径的服务费率(L2 配置中心)
|
|
|
+ when(configService.getIntConfig(CfConfigKeys.COMMISSION_SERVICE_RATE, 1000)).thenReturn(1000);
|
|
|
+
|
|
|
+ // 用户ID → 家庭成员档案
|
|
|
+ when(familyMemberMapper.selectOne(any())).thenAnswer(inv -> {
|
|
|
+ LambdaQueryWrapper<FamilyMember> wrapper = inv.getArgument(0);
|
|
|
+ // MP 惰性绑定参数:必须先生成 SQL 片段,paramNameValuePairs 才有值
|
|
|
+ wrapper.getSqlSegment();
|
|
|
+ Long uid = wrapper.getParamNameValuePairs().values().stream()
|
|
|
+ .filter(v -> v instanceof Long)
|
|
|
+ .map(v -> (Long) v)
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ return uid == null ? null : member(uid);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 规则上限比例:F03a ≤ 70%,F03b ≤ 20%
|
|
|
+ when(cfEarnRuleService.getByCode("F03a"))
|
|
|
+ .thenReturn(earnRule("F03a", "{\"base\":\"ppoints\",\"tier_table\":\"cf_direct_sales_tier\","
|
|
|
+ + "\"window\":\"1y_rolling\",\"team_depth\":8,\"max_rate_percent\":70,\"downgrade\":true}"));
|
|
|
+ when(cfEarnRuleService.getByCode("F03b"))
|
|
|
+ .thenReturn(earnRule("F03b", "{\"base\":\"ppoints\",\"tier_table\":\"cf_indirect_sales_tier\","
|
|
|
+ + "\"window\":\"1y_rolling\",\"max_rate_percent\":20,\"downgrade\":true}"));
|
|
|
+
|
|
|
+ // P 点消费日志写入
|
|
|
+ when(ppointConsumptionLogMapper.insert(any())).thenReturn(1);
|
|
|
+ // 感恩记录相关
|
|
|
+ when(octopusKeyPersonMapper.selectList(any())).thenReturn(Collections.emptyList());
|
|
|
+ when(octopusKeyPersonMapper.insert(any())).thenAnswer(inv -> {
|
|
|
+ OctopusKeyPerson kp = inv.getArgument(0);
|
|
|
+ kp.setId(500L);
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+ when(octopusEffectRecordMapper.insert(any())).thenAnswer(inv -> {
|
|
|
+ OctopusEffectRecord record = inv.getArgument(0);
|
|
|
+ record.setId(600L);
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+ when(octopusEffectKeyPersonMapper.insert(any())).thenReturn(1);
|
|
|
}
|
|
|
|
|
|
+ // ---------------- 测试夹具 ----------------
|
|
|
+
|
|
|
private ReferralTree ref(Long parent, int level) {
|
|
|
ReferralTree r = new ReferralTree();
|
|
|
r.setParentId(parent);
|
|
|
@@ -88,67 +190,265 @@ class CfCommissionServiceSettleTest {
|
|
|
return u;
|
|
|
}
|
|
|
|
|
|
- private CfRateTier tier(int rate) {
|
|
|
- CfRateTier t = new CfRateTier();
|
|
|
+ private FamilyMember member(Long userId) {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ m.setId(memberIdOf(userId));
|
|
|
+ m.setUserId(userId);
|
|
|
+ m.setFamilyId(userId + 5000);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfDirectSalesTier directTier(String name, int min, int rate) {
|
|
|
+ CfDirectSalesTier t = new CfDirectSalesTier();
|
|
|
+ t.setTierName(name);
|
|
|
+ t.setMinAnnualPpoints(min);
|
|
|
+ t.setRatePercent(rate);
|
|
|
+ t.setEnabled(1);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfIndirectSalesTier indirectTier(String name, int min, int rate) {
|
|
|
+ CfIndirectSalesTier t = new CfIndirectSalesTier();
|
|
|
+ t.setTierName(name);
|
|
|
+ t.setMinSelfPpoints(min);
|
|
|
t.setRatePercent(rate);
|
|
|
+ t.setEnabled(1);
|
|
|
return t;
|
|
|
}
|
|
|
|
|
|
+ private CfEarnRule earnRule(String code, String paramsJson) {
|
|
|
+ CfEarnRule r = new CfEarnRule();
|
|
|
+ r.setCode(code);
|
|
|
+ r.setEnabled(1);
|
|
|
+ r.setRuleType("formula");
|
|
|
+ r.setFormula("floor(base_ppoints * rate_percent / 100)");
|
|
|
+ r.setParamsJson(paramsJson);
|
|
|
+ r.setDedupRefType(code);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CfEarnResult earnResult(int amount, boolean duplicated) {
|
|
|
+ CfEarnResult r = new CfEarnResult();
|
|
|
+ r.setEarned(true);
|
|
|
+ r.setAmount(amount);
|
|
|
+ r.setDuplicated(duplicated);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
private void mockReferralChain(ReferralTree... nodes) {
|
|
|
when(referralTreeMapper.selectList(any(LambdaQueryWrapper.class)))
|
|
|
.thenReturn(Arrays.asList(nodes));
|
|
|
}
|
|
|
|
|
|
+ // ---------------- 测试 ----------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_两级销售奖_F03a按团队档位_F03b按自身档位() {
|
|
|
+ mockReferralChain(ref(10L, 1), ref(20L, 2));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
|
|
|
+ // F03a:团队 8 层滚动一年累计 2000 P 点 → 黄金档 40%
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(2000);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(2000)).thenReturn(directTier("黄金", 2000, 40));
|
|
|
+ // F03b:自身滚动一年累计 500 P 点 → 白银档 5%
|
|
|
+ when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(500);
|
|
|
+ when(cfReferralService.matchIndirectSalesTier(500)).thenReturn(indirectTier("白银", 500, 5));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(40, false));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(5, false));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ // F03a:基数 100 P 点 × 40%
|
|
|
+ ArgumentCaptor<Map<String, Object>> ctxCaptor = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), ctxCaptor.capture());
|
|
|
+ assertEquals(100, ctxCaptor.getValue().get("base_ppoints"));
|
|
|
+ assertEquals(40, ctxCaptor.getValue().get("rate_percent"));
|
|
|
+
|
|
|
+ // F03b:基数 100 P 点 × 5%
|
|
|
+ ArgumentCaptor<Map<String, Object>> ctxCaptor2 = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), ctxCaptor2.capture());
|
|
|
+ assertEquals(100, ctxCaptor2.getValue().get("base_ppoints"));
|
|
|
+ assertEquals(5, ctxCaptor2.getValue().get("rate_percent"));
|
|
|
+
|
|
|
+ // 买家 F02 返 CF(本期口径不变):100 元 × 100P点/100 × 10% = 10
|
|
|
+ verify(platformPointsService).earn(eq(BUYER_ID), eq(10), eq("order_consume"), eq(ORDER_ID), anyString());
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
- void settle_同家庭推荐人跳过_只返第一个非同家庭() {
|
|
|
- // 链路:level1(10) 同家庭 → 跳过;level2(20) 非同家庭 → 发放后终止;level3(30) 不应再发放
|
|
|
+ void settle_销售奖只走cfBalance_不再写旧流转记录() {
|
|
|
+ mockReferralChain(ref(10L, 1), ref(20L, 2));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
|
|
|
+ when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
|
|
|
+ .thenReturn(earnResult(10, false));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ // cf_transfer_record 只写买家 F02 一笔,销售奖不再产生第二套流水
|
|
|
+ verify(cfTransferRecordMapper, times(1)).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_同家庭引荐人跳过_顺延到下一级拿F03a() {
|
|
|
mockReferralChain(ref(10L, 1), ref(20L, 2), ref(30L, 3));
|
|
|
when(userMapper.selectById(10L)).thenReturn(user(10L, BUYER_FAMILY));
|
|
|
when(userMapper.selectById(20L)).thenReturn(user(20L, 200L));
|
|
|
when(userMapper.selectById(30L)).thenReturn(user(30L, 300L));
|
|
|
- // 让 level2、level3 都有有效阶梯比例,以区分"只返第一个"与"全层级都返"
|
|
|
- when(cfReferralService.getTotalTeamSize(20L)).thenReturn(5);
|
|
|
- when(cfReferralService.matchRateTier(5)).thenReturn(tier(10));
|
|
|
- when(cfReferralService.getTotalTeamSize(30L)).thenReturn(5);
|
|
|
- when(cfReferralService.matchRateTier(5)).thenReturn(tier(10));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(20L, 8)).thenReturn(500);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(500)).thenReturn(directTier("白银", 500, 25));
|
|
|
+ when(cfReferralService.getSelfAnnualPpoints(30L)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(20L)), eq("F03a"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(25, false));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(30L)), eq("F03b"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(2, false));
|
|
|
|
|
|
cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
|
|
|
- // 当前人返 CF:100元 × 100P点/100 × 10% = 10
|
|
|
- verify(platformPointsService).earn(eq(BUYER_ID), eq(10), eq("order_consume"), eq(ORDER_ID), anyString());
|
|
|
- // 只返 level2:100P点 × 10% = 10,一次
|
|
|
- verify(platformPointsService).earn(eq(20L), eq(10), eq("referral_dist"), eq(ORDER_ID), anyString());
|
|
|
- // level1 同家庭、level3 更远推荐人均不分润
|
|
|
- verify(platformPointsService, never()).earn(eq(10L), anyInt(), eq("referral_dist"), eq(ORDER_ID), anyString());
|
|
|
- verify(platformPointsService, never()).earn(eq(30L), anyInt(), eq("referral_dist"), eq(ORDER_ID), anyString());
|
|
|
+ // 同家庭的 level1 不发放
|
|
|
+ verify(cfBalanceService, never()).earnByRule(eq(memberIdOf(10L)), anyString(), anyLong(), any());
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03a"), eq(ORDER_ID), any());
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(30L)), eq("F03b"), eq(ORDER_ID), any());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void settle_首级即非同家庭_只返首级不再上溯() {
|
|
|
- // 链路:level1(10) 非同家庭、level2(20) 非同家庭 → 只返 level1,不再上溯 level2
|
|
|
- mockReferralChain(ref(10L, 1), ref(20L, 2));
|
|
|
+ void settle_最多两级_第三级引荐人不发放() {
|
|
|
+ mockReferralChain(ref(10L, 1), ref(20L, 2), ref(30L, 3));
|
|
|
when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
|
|
|
- when(cfReferralService.getTotalTeamSize(10L)).thenReturn(3);
|
|
|
- when(cfReferralService.matchRateTier(3)).thenReturn(tier(10));
|
|
|
- // level2 也有有效比例,当前"全层级都返"实现会误发 level2
|
|
|
- when(cfReferralService.getTotalTeamSize(20L)).thenReturn(5);
|
|
|
- when(cfReferralService.matchRateTier(5)).thenReturn(tier(10));
|
|
|
+ when(userMapper.selectById(30L)).thenReturn(user(30L, 400L));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
|
|
|
+ when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
|
|
|
+ .thenReturn(earnResult(10, false));
|
|
|
|
|
|
cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
|
|
|
- verify(platformPointsService).earn(eq(10L), eq(10), eq("referral_dist"), eq(ORDER_ID), anyString());
|
|
|
- verify(platformPointsService, never()).earn(eq(20L), anyInt(), eq("referral_dist"), eq(ORDER_ID), anyString());
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any());
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), any());
|
|
|
+ verify(cfBalanceService, never()).earnByRule(eq(memberIdOf(30L)), anyString(), anyLong(), any());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void settle_全链路同家庭_无人分润() {
|
|
|
- mockReferralChain(ref(10L, 1), ref(20L, 2));
|
|
|
- when(userMapper.selectById(10L)).thenReturn(user(10L, BUYER_FAMILY));
|
|
|
- when(userMapper.selectById(20L)).thenReturn(user(20L, BUYER_FAMILY));
|
|
|
+ void settle_档位比例超规则上限_按上限封顶() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ // 运营把档位误配到 90% → 按 F03a 的 max_rate_percent=70 封顶
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(10000);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(10000)).thenReturn(directTier("钻石", 10000, 90));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(70, false));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), captor.capture());
|
|
|
+ assertEquals(70, captor.getValue().get("rate_percent"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_无匹配档位_不发放销售奖() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(99);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(99)).thenReturn(null);
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ verify(cfBalanceService, never()).earnByRule(anyLong(), anyString(), anyLong(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_落P点消费日志_基数与订单对应() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
|
|
|
+ .thenReturn(earnResult(10, false));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ ArgumentCaptor<PpointConsumptionLog> captor = ArgumentCaptor.forClass(PpointConsumptionLog.class);
|
|
|
+ verify(ppointConsumptionLogMapper).insert(captor.capture());
|
|
|
+ PpointConsumptionLog log = captor.getValue();
|
|
|
+ assertEquals(BUYER_ID, log.getUserId());
|
|
|
+ assertEquals(ORDER_ID, log.getOrderId());
|
|
|
+ assertEquals("product", log.getOrderType());
|
|
|
+ assertEquals(PRODUCT_ID, log.getProductId());
|
|
|
+ assertEquals(10000, log.getAmountCent());
|
|
|
+ assertEquals(100, log.getPpoints());
|
|
|
+ assertNotNull(log.getCreatedAt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_F03a发放_自动创建感恩记录() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(userMapper.selectById(BUYER_ID)).thenReturn(user(BUYER_ID, BUYER_FAMILY));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(10, false));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ verify(octopusEffectRecordMapper).insert(any());
|
|
|
+ verify(octopusEffectKeyPersonMapper).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_幂等命中_不重复创建感恩记录() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(userMapper.selectById(BUYER_ID)).thenReturn(user(BUYER_ID, BUYER_FAMILY));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ // 幂等命中:earned=true 且 duplicated=true
|
|
|
+ when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
|
|
|
+ .thenReturn(earnResult(10, true));
|
|
|
+
|
|
|
+ cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
+
|
|
|
+ verify(octopusEffectRecordMapper, never()).insert(any());
|
|
|
+ verify(octopusEffectKeyPersonMapper, never()).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settleReferrerOnly_只发销售奖_不返买家CF() {
|
|
|
+ mockReferralChain(ref(10L, 1));
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
|
|
|
+ when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
|
|
|
+ when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
|
|
|
+ when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
|
|
|
+ .thenReturn(earnResult(10, false));
|
|
|
+
|
|
|
+ cfCommissionService.settleReferrerOnly(ORDER_ID, "membership", BUYER_ID, BUYER_FAMILY, 10000);
|
|
|
+
|
|
|
+ verify(platformPointsService, never()).earn(anyLong(), org.mockito.ArgumentMatchers.anyInt(),
|
|
|
+ anyString(), any(), anyString());
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any());
|
|
|
+ // 非商品订单基数:100 元 × 1000bps / 10000 = 10 P 点
|
|
|
+ ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
|
|
|
+ verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), captor.capture());
|
|
|
+ assertEquals(10, captor.getValue().get("base_ppoints"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void settle_无推荐链_不发放也不报错() {
|
|
|
+ mockReferralChain();
|
|
|
|
|
|
cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
|
|
|
|
|
|
- verify(platformPointsService, never()).earn(anyLong(), anyInt(), eq("referral_dist"), any(), anyString());
|
|
|
+ verify(cfBalanceService, never()).earnByRule(anyLong(), anyString(), anyLong(), any());
|
|
|
+ assertTrue(true);
|
|
|
}
|
|
|
}
|