|
@@ -0,0 +1,163 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.getInt;
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.getLong;
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.getString;
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.invoke;
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.newInstance;
|
|
|
|
|
+import static com.etotem.cfc.service.ContractReflect.requireClass;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测(RED,S-cfc-41 / team.profit.settle · TeamProfitService 8 份分佣 §4.2)。
|
|
|
|
|
+ * 规格:docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7 §12-T2 + §4.2(#5 拍板)+ §7.3 + §9#4
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约(本测试定义,GREEN 实现须满足)</b>:</p>
|
|
|
|
|
+ * <pre>
|
|
|
|
|
+ * TeamProfitService.allocateEightShares(Long poolId, Long orderId, String orderType, Long providerUserId,
|
|
|
|
|
+ * long eightSharePoolCent, long[] recipientUserIds, int[] layerStarLevels)
|
|
|
|
|
+ * : List<TeamShareRecord>
|
|
|
|
|
+ * layerStarLevels[i] = 第 i+1 层成员当期星级快照(§4.3,1~6);0 = 该层无人(§9#4 → 回流,不产出记录)
|
|
|
|
|
+ * recipientUserIds[i] = 第 i+1 层成员 userId(与 star 对齐;无人层位忽略)
|
|
|
|
|
+ * </pre>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>分配规则(§4.2 + #5 已拍板 v0.7)</b>:</p>
|
|
|
|
|
+ * <pre>
|
|
|
|
|
+ * 剩余份数 = shareTotal(默认 8,§7.8 team_profit.share_total)
|
|
|
|
|
+ * L1(服务者本人) 分得 min(L1星级, 剩余);剩余 -= 已分
|
|
|
|
|
+ * L2(引荐人) 分得 min(L2星级, 剩余);剩余 -= 已分
|
|
|
|
|
+ * L3(再上级) 分得 min(L3星级, 剩余);剩余 -= 已分
|
|
|
|
|
+ * 每层最多 6 份(六星上限,#5 拍板);层级数动态浮动:最少 2 层、最多 3 层
|
|
|
|
|
+ * 金额 shareCent = round(8份池 × 份数 / 8)
|
|
|
|
|
+ * 剩余 > 0 且层级已尽 → 回流团队奖池/平台(§4.2/§4.5),不产出记录
|
|
|
|
|
+ * 唯一键幂等:team_share_record.uk_order_rec (order_id, recipient_user_id, layer)(§8.3,建表约束)
|
|
|
|
|
+ * </pre>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>权限流不适用(服务层契约,N/A)。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class TeamShareRecordEightShareContractRedTest {
|
|
|
|
|
+
|
|
|
|
|
+ /** 契约方法签名常量 */
|
|
|
|
|
+ private static final String M_ALLOCATE = "allocateEightShares";
|
|
|
|
|
+ private static final Class<?>[] ALLOCATE_PARAMS =
|
|
|
|
|
+ {Long.class, Long.class, String.class, Long.class, long.class, long[].class, int[].class};
|
|
|
|
|
+
|
|
|
|
|
+ private static final long POOL_ID = 1001L;
|
|
|
|
|
+ private static final long ORDER_ID = 9001L;
|
|
|
|
|
+ private static final String ORDER_TYPE = "service";
|
|
|
|
|
+ private static final long PROVIDER = 101L;
|
|
|
|
|
+ private static final long EIGHT_POOL = 4080L; // §2 示例 T×80% = 51×80% = 40.8元 = 4080分
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 契约实例 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private Object service() {
|
|
|
|
|
+ return newInstance(requireClass(TeamProfitContract.SERVICE_TEAM, "§12-T2/§4.2"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 调用契约方法;返回 List<TeamShareRecord>(按 layer 升序) */
|
|
|
|
|
+ private List<Object> allocate(long[] recipients, int[] stars) {
|
|
|
|
|
+ Object result = invoke(service(), M_ALLOCATE, ALLOCATE_PARAMS,
|
|
|
|
|
+ new Object[]{POOL_ID, ORDER_ID, ORDER_TYPE, PROVIDER, EIGHT_POOL, recipients, stars});
|
|
|
|
|
+ assertNotNull(result, "契约: allocateEightShares 应返回 List<TeamShareRecord>");
|
|
|
|
|
+ if (!(result instanceof List)) {
|
|
|
|
|
+ fail("RED: allocateEightShares 返回类型应为 List<TeamShareRecord>,实际 " + result.getClass());
|
|
|
|
|
+ }
|
|
|
|
|
+ return (List<Object>) result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常流 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** AC(§4.2 用户示例): 6★自服务(6份) + 1★引荐人(1份) + 1★再上级(1份) → 30.6元/5.1元/5.1元 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void userExample_6star_1star_1star_threeLayers() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 102L, 103L}, new int[]{6, 1, 1});
|
|
|
|
|
+ assertEquals(3, records.size(), "应产出 3 条记录(L1/L2/L3)");
|
|
|
|
|
+ assertLayer(records.get(0), 1, PROVIDER, 101L, 6, 6, 3060L);
|
|
|
|
|
+ assertLayer(records.get(1), 2, PROVIDER, 102L, 1, 1, 510L);
|
|
|
|
|
+ assertLayer(records.get(2), 3, PROVIDER, 103L, 1, 1, 510L);
|
|
|
|
|
+ assertEquals(EIGHT_POOL,
|
|
|
|
|
+ getLong(records.get(0), "shareCent") + getLong(records.get(1), "shareCent")
|
|
|
|
|
+ + getLong(records.get(2), "shareCent"),
|
|
|
|
|
+ "8 份分完,金额合计 = 8份池 4080");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** AC(#5 拍板): 每层按星级拿份数;6★+2★ 恰好 8 份 → 最少 2 层即分完,第 3 层无记录 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void minTwoLayers_6star_2star_exhaustAllEight() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 102L, 0L}, new int[]{6, 2, 0});
|
|
|
|
|
+ assertEquals(2, records.size(), "6+2=8 分完,无需第 3 层(最少 2 层,#5 拍板)");
|
|
|
|
|
+ assertLayer(records.get(0), 1, PROVIDER, 101L, 6, 6, 3060L);
|
|
|
|
|
+ assertLayer(records.get(1), 2, PROVIDER, 102L, 2, 2, 1020L);
|
|
|
|
|
+ assertEquals(EIGHT_POOL,
|
|
|
|
|
+ getLong(records.get(0), "shareCent") + getLong(records.get(1), "shareCent"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** AC(#5 拍板): 每层最多 6 份;6★+6★ 时第 2 层取 min(6, 剩余2)=2 份,3 层不再产出 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void capByRemaining_6star_6star_thirdLayerExhausted() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 102L, 103L}, new int[]{6, 6, 1});
|
|
|
|
|
+ assertEquals(2, records.size(), "L1 6份 + L2 min(6,剩余2)=2份 → 8 份分完,L3 无记录");
|
|
|
|
|
+ assertLayer(records.get(0), 1, PROVIDER, 101L, 6, 6, 3060L);
|
|
|
|
|
+ assertLayer(records.get(1), 2, PROVIDER, 102L, 6, 2, 1020L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 边界流 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** AC: 每层 min(星级, 剩余);三星全 1★ → 只用 3 份,剩余 5 份回流(不产出记录,金额合计 < 池) */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void allOneStar_remainingRefluxToFund() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 102L, 103L}, new int[]{1, 1, 1});
|
|
|
|
|
+ assertEquals(3, records.size());
|
|
|
|
|
+ assertLayer(records.get(0), 1, PROVIDER, 101L, 1, 1, 510L);
|
|
|
|
|
+ assertLayer(records.get(1), 2, PROVIDER, 102L, 1, 1, 510L);
|
|
|
|
|
+ assertLayer(records.get(2), 3, PROVIDER, 103L, 1, 1, 510L);
|
|
|
|
|
+ assertEquals(1530L,
|
|
|
|
|
+ getLong(records.get(0), "shareCent") + getLong(records.get(1), "shareCent")
|
|
|
|
|
+ + getLong(records.get(2), "shareCent"),
|
|
|
|
|
+ "3 份发放 1530 分;剩余 5 份回流团队奖池/平台(§4.2/§4.5)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** AC(§9#4): 服务提供者无引荐人/无团队 → 该层无人,份额回流;仅服务者本人记录 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void loneProvider_3star_remainingReflux() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 0L, 0L}, new int[]{3, 0, 0});
|
|
|
|
|
+ assertEquals(1, records.size(), "仅 L1 有记录,L2/L3 无人回流");
|
|
|
|
|
+ assertLayer(records.get(0), 1, PROVIDER, 101L, 3, 3, 1530L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** AC(§7.3): 记录携带契约列(poolId/orderId/orderType/providerUserId/recipient/layer/starLevel/shares/shareCent/status) */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void recordFields_carryContractColumns() {
|
|
|
|
|
+ List<Object> records = allocate(new long[]{101L, 102L, 103L}, new int[]{4, 2, 1});
|
|
|
|
|
+ assertEquals(3, records.size());
|
|
|
|
|
+ Object r = records.get(0);
|
|
|
|
|
+ assertEquals(POOL_ID, getLong(r, "poolId"));
|
|
|
|
|
+ assertEquals(ORDER_ID, getLong(r, "orderId"));
|
|
|
|
|
+ assertEquals(ORDER_TYPE, getString(r, "orderType"));
|
|
|
|
|
+ assertEquals(PROVIDER, getLong(r, "providerUserId"));
|
|
|
|
|
+ assertEquals(101L, getLong(r, "recipientUserId"));
|
|
|
|
|
+ assertEquals(1, getInt(r, "layer"));
|
|
|
|
|
+ assertEquals(4, getInt(r, "starLevel"), "当期星级快照(§4.3/§9#8)");
|
|
|
|
|
+ assertEquals(4, getInt(r, "shares"));
|
|
|
|
|
+ assertEquals(2040L, getLong(r, "shareCent"), "4080×4/8 = 2040");
|
|
|
|
|
+ assertEquals("pending", getString(r, "status"), "§8.1/§7.3 初始状态 pending");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 断言工具 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static void assertLayer(Object r, int layer, long provider, long recipient,
|
|
|
|
|
+ int star, int shares, long cent) {
|
|
|
|
|
+ assertEquals(layer, getInt(r, "layer"), "layer");
|
|
|
|
|
+ assertEquals(provider, getLong(r, "providerUserId"), "providerUserId");
|
|
|
|
|
+ assertEquals(recipient, getLong(r, "recipientUserId"), "recipientUserId");
|
|
|
|
|
+ assertEquals(star, getInt(r, "starLevel"), "starLevel(当期星级快照)");
|
|
|
|
|
+ assertEquals(shares, getInt(r, "shares"), "shares");
|
|
|
|
|
+ assertEquals(cent, getLong(r, "shareCent"), "shareCent(8份池×份数/8)");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|