CfCommissionServiceSettleTest.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.MybatisConfiguration;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
  5. import com.etotem.cfc.config.ConfigService;
  6. import com.etotem.cfc.constant.CfConfigKeys;
  7. import com.etotem.cfc.dto.CfEarnResult;
  8. import com.etotem.cfc.entity.CfDirectSalesTier;
  9. import com.etotem.cfc.entity.CfEarnRule;
  10. import com.etotem.cfc.entity.CfIndirectSalesTier;
  11. import com.etotem.cfc.entity.FamilyMember;
  12. import com.etotem.cfc.entity.OctopusEffectRecord;
  13. import com.etotem.cfc.entity.OctopusKeyPerson;
  14. import com.etotem.cfc.entity.PpointConsumptionLog;
  15. import com.etotem.cfc.entity.Product;
  16. import com.etotem.cfc.entity.ReferralTree;
  17. import com.etotem.cfc.entity.User;
  18. import com.etotem.cfc.mapper.CfTransferRecordMapper;
  19. import com.etotem.cfc.mapper.FamilyMemberMapper;
  20. import com.etotem.cfc.mapper.OctopusEffectKeyPersonMapper;
  21. import com.etotem.cfc.mapper.OctopusEffectRecordMapper;
  22. import com.etotem.cfc.mapper.OctopusKeyPersonMapper;
  23. import com.etotem.cfc.mapper.PpointConsumptionLogMapper;
  24. import com.etotem.cfc.mapper.ProductMapper;
  25. import com.etotem.cfc.mapper.ReferralTreeMapper;
  26. import com.etotem.cfc.mapper.UserMapper;
  27. import com.fasterxml.jackson.databind.ObjectMapper;
  28. import org.apache.ibatis.builder.MapperBuilderAssistant;
  29. import org.junit.jupiter.api.BeforeEach;
  30. import org.junit.jupiter.api.Test;
  31. import org.mockito.ArgumentCaptor;
  32. import org.mockito.InjectMocks;
  33. import org.mockito.Mock;
  34. import org.mockito.MockitoAnnotations;
  35. import org.mockito.Spy;
  36. import java.util.Arrays;
  37. import java.util.Collections;
  38. import java.util.Map;
  39. import static org.junit.jupiter.api.Assertions.assertEquals;
  40. import static org.junit.jupiter.api.Assertions.assertNotNull;
  41. import static org.junit.jupiter.api.Assertions.assertTrue;
  42. import static org.mockito.ArgumentMatchers.any;
  43. import static org.mockito.ArgumentMatchers.anyLong;
  44. import static org.mockito.ArgumentMatchers.anyString;
  45. import static org.mockito.ArgumentMatchers.eq;
  46. import static org.mockito.Mockito.never;
  47. import static org.mockito.Mockito.times;
  48. import static org.mockito.Mockito.verify;
  49. import static org.mockito.Mockito.when;
  50. /**
  51. * CF 分佣收口(F-4)两级销售奖结算行为测试。
  52. *
  53. * <p>覆盖口径(计划 §四 / §3.1 / §4.2 / §4.4):</p>
  54. * <ul>
  55. * <li>第 1 个非同家庭引荐人 → F03a 直接销售奖,比例取"团队 8 层内滚动一年累计 P 点"档位;</li>
  56. * <li>第 2 个非同家庭引荐人 → F03b 间接销售奖,比例取"自身滚动一年累计消费 P 点"档位;</li>
  57. * <li>共同基数 = 本单消费产生的 P 点(商品按有效 P 点比例折算);</li>
  58. * <li>同家庭互推(D5)跳过并顺延,最多两级;</li>
  59. * <li>档位比例超 {@code cf_earn_rule.params_json.max_rate_percent} 时按上限封顶;</li>
  60. * <li>发放统一走 F-2 的 {@code earnByRule}(幂等键复用 F-2 唯一键);销售奖不再写第二套流水
  61. * ({@code cf_transfer_record} 仅买家 F02 路径写);</li>
  62. * <li>F03a 发放时创建感恩记录,幂等命中(duplicated)不重复创建。</li>
  63. * </ul>
  64. */
  65. class CfCommissionServiceSettleTest {
  66. @Mock
  67. private ReferralTreeMapper referralTreeMapper;
  68. @Mock
  69. private UserMapper userMapper;
  70. @Mock
  71. private CfReferralService cfReferralService;
  72. @Mock
  73. private PlatformPointsService platformPointsService;
  74. @Mock
  75. private CfTransferRecordMapper cfTransferRecordMapper;
  76. @Mock
  77. private PpointConfigService ppointConfigService;
  78. @Mock
  79. private SysConfigService sysConfigService;
  80. @Mock
  81. private ProductMapper productMapper;
  82. @Mock
  83. private OctopusKeyPersonMapper octopusKeyPersonMapper;
  84. @Mock
  85. private OctopusEffectRecordMapper octopusEffectRecordMapper;
  86. @Mock
  87. private OctopusEffectKeyPersonMapper octopusEffectKeyPersonMapper;
  88. @Mock
  89. private CfBalanceService cfBalanceService;
  90. @Mock
  91. private CfEarnRuleService cfEarnRuleService;
  92. @Mock
  93. private FamilyMemberMapper familyMemberMapper;
  94. @Mock
  95. private PpointConsumptionLogMapper ppointConsumptionLogMapper;
  96. @Mock
  97. private ConfigService configService;
  98. @Spy
  99. private ObjectMapper objectMapper = new ObjectMapper();
  100. @InjectMocks
  101. private CfCommissionService cfCommissionService;
  102. private static final Long ORDER_ID = 1L;
  103. private static final Long PRODUCT_ID = 999L;
  104. private static final Long BUYER_ID = 100L;
  105. private static final Long BUYER_FAMILY = 100L;
  106. /** 家庭成员ID = 用户ID + 1000,便于断言 */
  107. private static Long memberIdOf(Long userId) {
  108. return userId + 1000;
  109. }
  110. @BeforeEach
  111. void setUp() {
  112. MockitoAnnotations.openMocks(this);
  113. TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), ReferralTree.class);
  114. TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyMember.class);
  115. TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), OctopusKeyPerson.class);
  116. // 商品:有效 P 点=100(商品订单 100 元 → 基数 100 P 点)
  117. Product p = new Product();
  118. p.setId(PRODUCT_ID);
  119. p.setCategoryId(1L);
  120. when(productMapper.selectById(PRODUCT_ID)).thenReturn(p);
  121. when(ppointConfigService.getEffectivePpoint(PRODUCT_ID, 1L)).thenReturn(100);
  122. // 买家 F02 路径(本期不改口径)
  123. when(sysConfigService.getValue("product_platform_points_share")).thenReturn("1000");
  124. when(cfTransferRecordMapper.insert(any())).thenReturn(1);
  125. // 非商品路径的服务费率(L2 配置中心)
  126. when(configService.getIntConfig(CfConfigKeys.COMMISSION_SERVICE_RATE, 1000)).thenReturn(1000);
  127. // 用户ID → 家庭成员档案
  128. when(familyMemberMapper.selectOne(any())).thenAnswer(inv -> {
  129. LambdaQueryWrapper<FamilyMember> wrapper = inv.getArgument(0);
  130. // MP 惰性绑定参数:必须先生成 SQL 片段,paramNameValuePairs 才有值
  131. wrapper.getSqlSegment();
  132. Long uid = wrapper.getParamNameValuePairs().values().stream()
  133. .filter(v -> v instanceof Long)
  134. .map(v -> (Long) v)
  135. .findFirst()
  136. .orElse(null);
  137. return uid == null ? null : member(uid);
  138. });
  139. // 规则上限比例:F03a ≤ 70%,F03b ≤ 20%
  140. when(cfEarnRuleService.getByCode("F03a"))
  141. .thenReturn(earnRule("F03a", "{\"base\":\"ppoints\",\"tier_table\":\"cf_direct_sales_tier\","
  142. + "\"window\":\"1y_rolling\",\"team_depth\":8,\"max_rate_percent\":70,\"downgrade\":true}"));
  143. when(cfEarnRuleService.getByCode("F03b"))
  144. .thenReturn(earnRule("F03b", "{\"base\":\"ppoints\",\"tier_table\":\"cf_indirect_sales_tier\","
  145. + "\"window\":\"1y_rolling\",\"max_rate_percent\":20,\"downgrade\":true}"));
  146. // P 点消费日志写入
  147. when(ppointConsumptionLogMapper.insert(any())).thenReturn(1);
  148. // 感恩记录相关
  149. when(octopusKeyPersonMapper.selectList(any())).thenReturn(Collections.emptyList());
  150. when(octopusKeyPersonMapper.insert(any())).thenAnswer(inv -> {
  151. OctopusKeyPerson kp = inv.getArgument(0);
  152. kp.setId(500L);
  153. return 1;
  154. });
  155. when(octopusEffectRecordMapper.insert(any())).thenAnswer(inv -> {
  156. OctopusEffectRecord record = inv.getArgument(0);
  157. record.setId(600L);
  158. return 1;
  159. });
  160. when(octopusEffectKeyPersonMapper.insert(any())).thenReturn(1);
  161. }
  162. // ---------------- 测试夹具 ----------------
  163. private ReferralTree ref(Long parent, int level) {
  164. ReferralTree r = new ReferralTree();
  165. r.setParentId(parent);
  166. r.setChildId(BUYER_ID);
  167. r.setLevel(level);
  168. return r;
  169. }
  170. private User user(Long id, Long familyId) {
  171. User u = new User();
  172. u.setId(id);
  173. u.setFamilyId(familyId);
  174. return u;
  175. }
  176. private FamilyMember member(Long userId) {
  177. FamilyMember m = new FamilyMember();
  178. m.setId(memberIdOf(userId));
  179. m.setUserId(userId);
  180. m.setFamilyId(userId + 5000);
  181. return m;
  182. }
  183. private CfDirectSalesTier directTier(String name, int min, int rate) {
  184. CfDirectSalesTier t = new CfDirectSalesTier();
  185. t.setTierName(name);
  186. t.setMinAnnualPpoints(min);
  187. t.setRatePercent(rate);
  188. t.setEnabled(1);
  189. return t;
  190. }
  191. private CfIndirectSalesTier indirectTier(String name, int min, int rate) {
  192. CfIndirectSalesTier t = new CfIndirectSalesTier();
  193. t.setTierName(name);
  194. t.setMinSelfPpoints(min);
  195. t.setRatePercent(rate);
  196. t.setEnabled(1);
  197. return t;
  198. }
  199. private CfEarnRule earnRule(String code, String paramsJson) {
  200. CfEarnRule r = new CfEarnRule();
  201. r.setCode(code);
  202. r.setEnabled(1);
  203. r.setRuleType("formula");
  204. r.setFormula("floor(base_ppoints * rate_percent / 100)");
  205. r.setParamsJson(paramsJson);
  206. r.setDedupRefType(code);
  207. return r;
  208. }
  209. private CfEarnResult earnResult(int amount, boolean duplicated) {
  210. CfEarnResult r = new CfEarnResult();
  211. r.setEarned(true);
  212. r.setAmount(amount);
  213. r.setDuplicated(duplicated);
  214. return r;
  215. }
  216. private void mockReferralChain(ReferralTree... nodes) {
  217. when(referralTreeMapper.selectList(any(LambdaQueryWrapper.class)))
  218. .thenReturn(Arrays.asList(nodes));
  219. }
  220. // ---------------- 测试 ----------------
  221. @Test
  222. void settle_两级销售奖_F03a按团队档位_F03b按自身档位() {
  223. mockReferralChain(ref(10L, 1), ref(20L, 2));
  224. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  225. when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
  226. // F03a:团队 8 层滚动一年累计 2000 P 点 → 黄金档 40%
  227. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(2000);
  228. when(cfReferralService.matchDirectSalesTier(2000)).thenReturn(directTier("黄金", 2000, 40));
  229. // F03b:自身滚动一年累计 500 P 点 → 白银档 5%
  230. when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(500);
  231. when(cfReferralService.matchIndirectSalesTier(500)).thenReturn(indirectTier("白银", 500, 5));
  232. when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
  233. .thenReturn(earnResult(40, false));
  234. when(cfBalanceService.earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), any()))
  235. .thenReturn(earnResult(5, false));
  236. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  237. // F03a:基数 100 P 点 × 40%
  238. ArgumentCaptor<Map<String, Object>> ctxCaptor = ArgumentCaptor.forClass(Map.class);
  239. verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), ctxCaptor.capture());
  240. assertEquals(100, ctxCaptor.getValue().get("base_ppoints"));
  241. assertEquals(40, ctxCaptor.getValue().get("rate_percent"));
  242. // F03b:基数 100 P 点 × 5%
  243. ArgumentCaptor<Map<String, Object>> ctxCaptor2 = ArgumentCaptor.forClass(Map.class);
  244. verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), ctxCaptor2.capture());
  245. assertEquals(100, ctxCaptor2.getValue().get("base_ppoints"));
  246. assertEquals(5, ctxCaptor2.getValue().get("rate_percent"));
  247. // 买家 F02 返 CF(本期口径不变):100 元 × 100P点/100 × 10% = 10
  248. verify(platformPointsService).earn(eq(BUYER_ID), eq(10), eq("order_consume"), eq(ORDER_ID), anyString());
  249. }
  250. @Test
  251. void settle_销售奖只走cfBalance_不再写旧流转记录() {
  252. mockReferralChain(ref(10L, 1), ref(20L, 2));
  253. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  254. when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
  255. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  256. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  257. when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(100);
  258. when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
  259. when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
  260. .thenReturn(earnResult(10, false));
  261. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  262. // cf_transfer_record 只写买家 F02 一笔,销售奖不再产生第二套流水
  263. verify(cfTransferRecordMapper, times(1)).insert(any());
  264. }
  265. @Test
  266. void settle_同家庭引荐人跳过_顺延到下一级拿F03a() {
  267. mockReferralChain(ref(10L, 1), ref(20L, 2), ref(30L, 3));
  268. when(userMapper.selectById(10L)).thenReturn(user(10L, BUYER_FAMILY));
  269. when(userMapper.selectById(20L)).thenReturn(user(20L, 200L));
  270. when(userMapper.selectById(30L)).thenReturn(user(30L, 300L));
  271. when(cfReferralService.getTeamAnnualPpoints(20L, 8)).thenReturn(500);
  272. when(cfReferralService.matchDirectSalesTier(500)).thenReturn(directTier("白银", 500, 25));
  273. when(cfReferralService.getSelfAnnualPpoints(30L)).thenReturn(100);
  274. when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
  275. when(cfBalanceService.earnByRule(eq(memberIdOf(20L)), eq("F03a"), eq(ORDER_ID), any()))
  276. .thenReturn(earnResult(25, false));
  277. when(cfBalanceService.earnByRule(eq(memberIdOf(30L)), eq("F03b"), eq(ORDER_ID), any()))
  278. .thenReturn(earnResult(2, false));
  279. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  280. // 同家庭的 level1 不发放
  281. verify(cfBalanceService, never()).earnByRule(eq(memberIdOf(10L)), anyString(), anyLong(), any());
  282. verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03a"), eq(ORDER_ID), any());
  283. verify(cfBalanceService).earnByRule(eq(memberIdOf(30L)), eq("F03b"), eq(ORDER_ID), any());
  284. }
  285. @Test
  286. void settle_最多两级_第三级引荐人不发放() {
  287. mockReferralChain(ref(10L, 1), ref(20L, 2), ref(30L, 3));
  288. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  289. when(userMapper.selectById(20L)).thenReturn(user(20L, 300L));
  290. when(userMapper.selectById(30L)).thenReturn(user(30L, 400L));
  291. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  292. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  293. when(cfReferralService.getSelfAnnualPpoints(20L)).thenReturn(100);
  294. when(cfReferralService.matchIndirectSalesTier(100)).thenReturn(indirectTier("青铜", 100, 2));
  295. when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
  296. .thenReturn(earnResult(10, false));
  297. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  298. verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any());
  299. verify(cfBalanceService).earnByRule(eq(memberIdOf(20L)), eq("F03b"), eq(ORDER_ID), any());
  300. verify(cfBalanceService, never()).earnByRule(eq(memberIdOf(30L)), anyString(), anyLong(), any());
  301. }
  302. @Test
  303. void settle_档位比例超规则上限_按上限封顶() {
  304. mockReferralChain(ref(10L, 1));
  305. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  306. // 运营把档位误配到 90% → 按 F03a 的 max_rate_percent=70 封顶
  307. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(10000);
  308. when(cfReferralService.matchDirectSalesTier(10000)).thenReturn(directTier("钻石", 10000, 90));
  309. when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
  310. .thenReturn(earnResult(70, false));
  311. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  312. ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
  313. verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), captor.capture());
  314. assertEquals(70, captor.getValue().get("rate_percent"));
  315. }
  316. @Test
  317. void settle_无匹配档位_不发放销售奖() {
  318. mockReferralChain(ref(10L, 1));
  319. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  320. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(99);
  321. when(cfReferralService.matchDirectSalesTier(99)).thenReturn(null);
  322. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  323. verify(cfBalanceService, never()).earnByRule(anyLong(), anyString(), anyLong(), any());
  324. }
  325. @Test
  326. void settle_落P点消费日志_基数与订单对应() {
  327. mockReferralChain(ref(10L, 1));
  328. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  329. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  330. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  331. when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
  332. .thenReturn(earnResult(10, false));
  333. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  334. ArgumentCaptor<PpointConsumptionLog> captor = ArgumentCaptor.forClass(PpointConsumptionLog.class);
  335. verify(ppointConsumptionLogMapper).insert(captor.capture());
  336. PpointConsumptionLog log = captor.getValue();
  337. assertEquals(BUYER_ID, log.getUserId());
  338. assertEquals(ORDER_ID, log.getOrderId());
  339. assertEquals("product", log.getOrderType());
  340. assertEquals(PRODUCT_ID, log.getProductId());
  341. assertEquals(10000, log.getAmountCent());
  342. assertEquals(100, log.getPpoints());
  343. assertNotNull(log.getCreatedAt());
  344. }
  345. @Test
  346. void settle_F03a发放_自动创建感恩记录() {
  347. mockReferralChain(ref(10L, 1));
  348. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  349. when(userMapper.selectById(BUYER_ID)).thenReturn(user(BUYER_ID, BUYER_FAMILY));
  350. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  351. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  352. when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
  353. .thenReturn(earnResult(10, false));
  354. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  355. verify(octopusEffectRecordMapper).insert(any());
  356. verify(octopusEffectKeyPersonMapper).insert(any());
  357. }
  358. @Test
  359. void settle_幂等命中_不重复创建感恩记录() {
  360. mockReferralChain(ref(10L, 1));
  361. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  362. when(userMapper.selectById(BUYER_ID)).thenReturn(user(BUYER_ID, BUYER_FAMILY));
  363. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  364. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  365. // 幂等命中:earned=true 且 duplicated=true
  366. when(cfBalanceService.earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any()))
  367. .thenReturn(earnResult(10, true));
  368. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  369. verify(octopusEffectRecordMapper, never()).insert(any());
  370. verify(octopusEffectKeyPersonMapper, never()).insert(any());
  371. }
  372. @Test
  373. void settleReferrerOnly_只发销售奖_不返买家CF() {
  374. mockReferralChain(ref(10L, 1));
  375. when(userMapper.selectById(10L)).thenReturn(user(10L, 200L));
  376. when(cfReferralService.getTeamAnnualPpoints(10L, 8)).thenReturn(100);
  377. when(cfReferralService.matchDirectSalesTier(100)).thenReturn(directTier("青铜", 100, 10));
  378. when(cfBalanceService.earnByRule(anyLong(), anyString(), anyLong(), any()))
  379. .thenReturn(earnResult(10, false));
  380. cfCommissionService.settleReferrerOnly(ORDER_ID, "membership", BUYER_ID, BUYER_FAMILY, 10000);
  381. verify(platformPointsService, never()).earn(anyLong(), org.mockito.ArgumentMatchers.anyInt(),
  382. anyString(), any(), anyString());
  383. verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), any());
  384. // 非商品订单基数:100 元 × 1000bps / 10000 = 10 P 点
  385. ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
  386. verify(cfBalanceService).earnByRule(eq(memberIdOf(10L)), eq("F03a"), eq(ORDER_ID), captor.capture());
  387. assertEquals(10, captor.getValue().get("base_ppoints"));
  388. }
  389. @Test
  390. void settle_无推荐链_不发放也不报错() {
  391. mockReferralChain();
  392. cfCommissionService.settle(ORDER_ID, "product", BUYER_ID, BUYER_FAMILY, 10000, PRODUCT_ID);
  393. verify(cfBalanceService, never()).earnByRule(anyLong(), anyString(), anyLong(), any());
  394. assertTrue(true);
  395. }
  396. }