Просмотр исходного кода

test(team): S-cfc-42 RED 契约单测 team.profit.hook — 6 订单服务同点位并行调用 ProfitPoolService.settle()(§12-T3+§2.2),反射结构契约,6/7 红

Sisyphus Agent 5 дней назад
Родитель
Сommit
6d3d2cfaae
1 измененных файлов с 213 добавлено и 0 удалено
  1. 213 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitHookContractTest.java

+ 213 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitHookContractTest.java

@@ -0,0 +1,213 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:团队分润支付成功钩子(能力键 {@code team.profit.hook})。
+ *
+ * <p>契约来源(v0.7 §12-T3 + §2.2):
+ * <blockquote>
+ *   T3 集成(P0):将 <b>6 个订单服务支付成功钩子</b> 接入 {@code ProfitPoolService.settle()},
+ *   复用现有 {@code CfCommissionService.settle} 调用点,<b>同点位并行调用</b>,不新增支付链路改动;
+ *   触发条件(§2.2):可分配池 {@code D = P - O - F > 0} 才进入分润流程。
+ * </blockquote></p>
+ *
+ * <p>6 个调用点(src/main 现状核实):
+ * <ul>
+ *   <li>{@code PaymentService} → {@code cfCommissionService.settle(orderId, "package", ...)}</li>
+ *   <li>{@code ProductOrderService} → {@code cfCommissionService.settle(orderId, "product", ...)}</li>
+ *   <li>{@code AssessmentOrderService} → {@code cfCommissionService.settle(orderId, "assessment", ...)}</li>
+ *   <li>{@code MemberSubscriptionService} → {@code settleReferrerOnly(orderId, "subscription", ...)} ×2</li>
+ *   <li>{@code MembershipService} → {@code settleReferrerOnly(orderId, "membership", ...)}</li>
+ * </ul></p>
+ *
+ * <p><b>实现现状</b>:本分支 src/main 中 {@code ProfitPoolService} 类、{@code profit_pool_order} /
+ * {@code team_share_record} / {@code team_award_record} / {@code service_cost_config} 表均未落地
+ * (全库 0 引用,schema.sql 无对应建表)。因此本测试以「反射结构契约」探测实现缺失:
+ * 能编译通过、能运行、断言失败(红),且失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者创建 {@code ProfitPoolService}(含 {@code settle} 入口)、
+ * 建 4 张表与实体、并在 6 个订单服务同点位注入 {@code profitPoolService} 钩子后,本测试自动转绿;
+ * 届时可将结构探测升级为行为级 mock 验证({@code verify(profitPoolService).settle(...)})。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为后端内部服务钩子(支付成功回调 → 分润服务),无用户权限维度,
+ * 「权限(适用时)」三流中的权限流不适用,以说明性用例标注。</p>
+ */
+class TeamProfitHookContractTest {
+
+    /** 契约要求接入的分润服务(§12-T3) */
+    private static final String PROFIT_POOL_SERVICE = "com.etotem.cfc.service.ProfitPoolService";
+
+    /** 6 个调用点所在的 5 个订单服务类(MemberSubscriptionService 含 2 个调用点) */
+    private static final String[] ORDER_SERVICES = {
+            "com.etotem.cfc.service.PaymentService",
+            "com.etotem.cfc.service.ProductOrderService",
+            "com.etotem.cfc.service.AssessmentOrderService",
+            "com.etotem.cfc.service.MemberSubscriptionService",
+            "com.etotem.cfc.service.MembershipService"
+    };
+
+    /** §7.1/7.3/7.4/7.5 四张新表 */
+    private static final String[] PROFIT_TABLES = {
+            "profit_pool_order", "team_share_record", "team_award_record", "service_cost_config"
+    };
+
+    /** §7.1 profit_pool_order 表唯一键(幂等 §8.3) */
+    private static final String PROFIT_POOL_UNIQUE_KEY = "uk_order";
+
+    // ------------------------------------------------------------------
+    // 正常流:ProfitPoolService 服务类 + settle 入口应存在
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:ProfitPoolService 服务类应存在")
+    void profitPoolService_classExists() {
+        assertNotNull(loadClass(PROFIT_POOL_SERVICE),
+                "[team.profit.hook] ProfitPoolService 类不存在:契约要求 6 个订单服务支付成功钩子"
+                        + "同点位并行调用 ProfitPoolService.settle()(§12-T3)。实现未落地。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:ProfitPoolService 应提供 settle 入口")
+    void profitPoolService_hasSettleMethod() {
+        Class<?> clazz = loadClass(PROFIT_POOL_SERVICE);
+        if (clazz == null) {
+            fail("[team.profit.hook] ProfitPoolService 类不存在,无法验证 settle 入口(§12-T3)。");
+        }
+        boolean hasSettle = false;
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals("settle")) {
+                hasSettle = true;
+                break;
+            }
+        }
+        assertTrue(hasSettle,
+                "[team.profit.hook] ProfitPoolService 应提供 settle(...) 方法作为支付成功钩子入口(§12-T3)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:6 个订单服务应注入 ProfitPoolService 钩子(同点位)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-3:6 个订单服务应注入 ProfitPoolService 钩子")
+    void orderServices_injectProfitPoolServiceHook() {
+        for (String svc : ORDER_SERVICES) {
+            Class<?> clazz = loadClass(svc);
+            assertNotNull(clazz, "[team.profit.hook] 订单服务类不存在:" + svc);
+            boolean injected = false;
+            for (Field f : clazz.getDeclaredFields()) {
+                if (PROFIT_POOL_SERVICE.equals(f.getType().getName())) {
+                    injected = true;
+                    break;
+                }
+            }
+            assertTrue(injected,
+                    "[team.profit.hook] " + simpleName(svc)
+                            + " 未注入 ProfitPoolService 钩子:应在 CfCommissionService.settle 调用点"
+                            + " 同点位并行调用 ProfitPoolService.settle()(§12-T3)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:触发条件 §2.2(D>0 才分润)—— profit_pool_order 承载可分配池
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:profit_pool_order 实体应存在并承载可分配池 D(poolCent)")
+    void profitPoolOrder_entityCarriesPool() {
+        Class<?> poolEntity = loadClass("com.etotem.cfc.entity.ProfitPoolOrder");
+        if (poolEntity == null) {
+            fail("[team.profit.hook] ProfitPoolOrder 实体不存在:§7.1 要求 profit_pool_order 表记录"
+                    + "可分配池 D(pool_cent),触发条件 D>0 才分润(§2.2)。");
+        }
+        boolean hasPoolCent = false;
+        for (Field f : poolEntity.getDeclaredFields()) {
+            if (f.getName().equals("poolCent")) {
+                hasPoolCent = true;
+                break;
+            }
+        }
+        assertTrue(hasPoolCent,
+                "[team.profit.hook] ProfitPoolOrder 实体应含 poolCent 字段(对应 pool_cent,§7.1),"
+                        + "用于判定触发条件 D = P - O - F > 0(§2.2)。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:profit_pool_order 应含 uk_order 唯一键(幂等 §8.3)")
+    void profitPoolOrder_hasIdempotentUniqueKey() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("profit_pool_order"),
+                "[team.profit.hook] schema.sql 缺少 profit_pool_order 建表(§7.1)。");
+        assertTrue(schema.contains(PROFIT_POOL_UNIQUE_KEY),
+                "[team.profit.hook] profit_pool_order 应含 uk_order 唯一键,保证重复回调不重复发放(§8.3)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:四张新表应落地 schema.sql
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-3:四张新表应落地 schema.sql(§7.1/7.3/7.4/7.5)")
+    void schemaSql_containsFourProfitTables() {
+        String schema = readSchemaSql();
+        for (String table : PROFIT_TABLES) {
+            assertTrue(schema.contains(table),
+                    "[team.profit.hook] schema.sql 缺少 " + table + " 建表(§7.1/7.3/7.4/7.5)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):后端内部服务钩子,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部服务钩子,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「支付成功回调 → 分润服务」内部链路,无权限校验维度。
+        // 不产生断言失败;仅为三流覆盖标注「不适用」。
+        assertTrue(true, "权限流不适用:team.profit.hook 为内部服务钩子,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助
+    // ------------------------------------------------------------------
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private String simpleName(String fqcn) {
+        int idx = fqcn.lastIndexOf('.');
+        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
+    }
+
+    private String readSchemaSql() {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
+            if (is == null) {
+                return "";
+            }
+            try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
+                scanner.useDelimiter("\\A");
+                return scanner.hasNext() ? scanner.next() : "";
+            }
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}