|
@@ -15,7 +15,9 @@ import com.payment.platform.module.payment.leshouying.client.LeshouyingClient;
|
|
|
import com.payment.platform.module.payment.leshouying.config.LeshouyingProperties;
|
|
import com.payment.platform.module.payment.leshouying.config.LeshouyingProperties;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyAreaItemDTO;
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyAreaItemDTO;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyCommitMerchantInfoDTO;
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyCommitMerchantInfoDTO;
|
|
|
|
|
+import com.payment.platform.module.payment.leshouying.dto.LsyMerchantCreateDTO;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyMerchantEntity;
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyMerchantEntity;
|
|
|
|
|
+import com.payment.platform.module.payment.leshouying.enums.LsyMerchantCategory;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyMerchantMapper;
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyMerchantMapper;
|
|
|
import com.payment.platform.module.payment.leshouying.service.LsyMerchantService;
|
|
import com.payment.platform.module.payment.leshouying.service.LsyMerchantService;
|
|
|
import com.payment.platform.module.payment.leshouying.service.LsyOnboardingService;
|
|
import com.payment.platform.module.payment.leshouying.service.LsyOnboardingService;
|
|
@@ -456,6 +458,116 @@ class LeshouyingOnboardingControllerTest {
|
|
|
gateFirst.verify(onboardingService).commitMerchantInfo(any());
|
|
gateFirst.verify(onboardingService).commitMerchantInfo(any());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ──────────────── category 的线上契约(加记 34) ────────────────
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * ★ 正对照:枚举**常量名**从 JSON 绑得上 —— 这一条**今天就是绿的**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>它的作用是给下面两条定性:如果这一条也红,说明坏的是「整份报文压根没走到绑定」,
|
|
|
|
|
+ * 而不是「标签没被认」。反过来说,这一条绿**不能**证明标签也能绑 ——
|
|
|
|
|
+ * Jackson 默认只认常量名,而 {@code of()}(唯一认中文标签的那个)在改动前
|
|
|
|
|
+ * 在生产代码里**零调用方**。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void category_fromJson_byConstantName_isAccepted() throws Exception {
|
|
|
|
|
+ login(TENANT, false);
|
|
|
|
|
+ when(onboardingService.createMerchant(any())).thenReturn(MCH_NO);
|
|
|
|
|
+ when(merchantService.createOnboardingRow(any(), any(), any(), any())).thenReturn(row(MCH_NO));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post(PREFIX + "/create")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(createBodyWithCategory(LsyMerchantCategory.ENTERPRISE_PUBLIC.name())))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(0));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * ★★ 本节的要害:**中文标签**必须能从 JSON 绑上,且到达 service 的是那个枚举常量。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>【为什么必须走 JSON 绑定这条路】既有的
|
|
|
|
|
+ * {@code LsyMerchantRequiredFieldsTest.category_of_acceptsTheTwoDocumentedNames_elseNull}
|
|
|
|
|
+ * 是**直接调 {@code of()}** 的 —— 它**绿着**,而线上是坏的。直调 {@code of()} 钉不住任何东西:
|
|
|
|
|
+ * 它证明的是「这个方法认得标签」,而 defects 恰恰在「Jackson 会不会去调这个方法」。
|
|
|
|
|
+ * 所以这里必须 {@code POST /create} + 真报文,且用
|
|
|
|
|
+ * {@code ArgumentCaptor<LsyMerchantCreateDTO>} 抓 service 的**实参** ——
|
|
|
|
|
+ * 只断言「被调用过」的话,绑成 {@code null} 也照样绿。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ <b>反例先行(同一份报文形状,只换 category 的值)</b>:先跑一个谁都不是的值,
|
|
|
|
|
+ * 断言它落到 {@code null}、**不是**落到某个类别上 —— 否则「标签解成了 ENTERPRISE_PUBLIC」
|
|
|
|
|
+ * 有可能只是因为这条绑定路径对**任何**输入都给这个常量(那样下面那条断言就是恒真的)。
|
|
|
|
|
+ * 两条一起才说明「标签 → 那个常量」是一次真实的映射。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ 标签**逐字**取 {@code getLabel()},不在用例里手打中文:手打的话抄错一个字就变成
|
|
|
|
|
+ * 一条测不到东西的假钉子(而且它会以「红的」形态出现,最容易被顺手改对)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ {@code assertSame} 而不是 {@code assertEquals}:要断的就是「**那个**枚举常量」,
|
|
|
|
|
+ * 枚举是单例,{@code assertSame} 才是这句话的字面意思。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void category_fromJson_byChineseLabel_reachesServiceAsThatEnum() throws Exception {
|
|
|
|
|
+ login(TENANT, false);
|
|
|
|
|
+ when(onboardingService.createMerchant(any())).thenReturn(MCH_NO);
|
|
|
|
|
+ when(merchantService.createOnboardingRow(any(), any(), any(), any())).thenReturn(row(MCH_NO));
|
|
|
|
|
+
|
|
|
|
|
+ // 反例先行:连谁都不是的值
|
|
|
|
|
+ mockMvc.perform(post(PREFIX + "/create")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(createBodyWithCategory("这不是任何一个类别")))
|
|
|
|
|
+ .andExpect(status().isOk());
|
|
|
|
|
+
|
|
|
|
|
+ // 正例:文档给这七类起的中文名(与上游 account_type 说明栏里的名字逐字同名)
|
|
|
|
|
+ mockMvc.perform(post(PREFIX + "/create")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(createBodyWithCategory(LsyMerchantCategory.ENTERPRISE_PUBLIC.getLabel())))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(0));
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<LsyMerchantCreateDTO> captor = ArgumentCaptor.forClass(LsyMerchantCreateDTO.class);
|
|
|
|
|
+ verify(onboardingService, times(2)).createMerchant(captor.capture());
|
|
|
|
|
+ List<LsyMerchantCreateDTO> reached = captor.getAllValues();
|
|
|
|
|
+ assertNull(reached.get(0).getCategory(),
|
|
|
|
|
+ "绑不上的类别值必须落到 null(不是抛、也不是默认到某一个类别上)");
|
|
|
|
|
+ assertSame(LsyMerchantCategory.ENTERPRISE_PUBLIC, reached.get(1).getCategory(),
|
|
|
|
|
+ "中文标签「" + LsyMerchantCategory.ENTERPRISE_PUBLIC.getLabel()
|
|
|
|
|
+ + "」必须解成 ENTERPRISE_PUBLIC。实测: " + reached.get(1).getCategory());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * ★ 绑不上的类别值 ⇒ **400 + 中文标签**,不是 500「服务器内部错误」。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>【为什么这条要一个**真** service】400 不是端点给的,是服务层**必填矩阵**给的:
|
|
|
|
|
+ * 类别为 {@code null} 时矩阵报的是两个派生字段({@code merchant_type} / {@code account_type}),
|
|
|
|
|
+ * 渲染前逐名过 {@code LsyMerchantRequiredFields.LABELS}({@code merchant_type} → 「商户类型」)
|
|
|
|
|
+ * ⇒ 400。把 service mock 掉,这条路径整条消失:mock 会直接回一个商户号,
|
|
|
|
|
+ * 于是「类别没绑上」这件事在应答里**完全不可见**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ <b>判据是「不落 500」而不是「恰好 400」</b>:本仓 {@code BusinessException} 的既有出口是
|
|
|
|
|
+ * HTTP 200 + 业务码,而绑定失败会落 {@code GlobalExceptionHandler} 的兜底分支 ⇒ HTTP 500。
|
|
|
|
|
+ * 所以这里同时钉 HTTP 码与业务码 —— 只钉一个的话,「500 也可以是 200 吗」这种错法看不出来。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ 报文里**只给**三个键(企业号 / 商户名 / 类别),必填矩阵因此会报出很多缺项;
|
|
|
|
|
+ * 断言只取「商户类型」这一个标签,它证明的是**类别这一格**被矩阵看见了。
|
|
|
|
|
+ * ★ 零接触下游:矩阵排在凭据闸之后、上送之前,所以这一枪一个上游字节都没出海。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void category_garbageValue_is400WithTheChineseLabel_notServerError() throws Exception {
|
|
|
|
|
+ login(TENANT, false);
|
|
|
|
|
+ LeshouyingClient client = mock(LeshouyingClient.class);
|
|
|
|
|
+ LeshouyingProperties props = new LeshouyingProperties();
|
|
|
|
|
+ withOnboardingCredentials(props);
|
|
|
|
|
+ MockMvc mvc = standaloneMvc(new LeshouyingOnboardingController(merchantService,
|
|
|
|
|
+ new LsyOnboardingService(client, props)));
|
|
|
|
|
+
|
|
|
|
|
+ mvc.perform(post(PREFIX + "/create")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(createBodyWithCategory("这不是任何一个类别")))
|
|
|
|
|
+ .andExpect(status().isOk()) // 本仓 BusinessException 的既有出口
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(400))
|
|
|
|
|
+ .andExpect(jsonPath("$.msg", containsString("商户类型")));
|
|
|
|
|
+
|
|
|
|
|
+ verifyNoInteractions(client);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ──────────────── 省市区只读端点(加记 29 §二 裁定 R-T9a) ────────────────
|
|
// ──────────────── 省市区只读端点(加记 29 §二 裁定 R-T9a) ────────────────
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1262,6 +1374,36 @@ class LeshouyingOnboardingControllerTest {
|
|
|
return b;
|
|
return b;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 一份**真的走 JSON 绑定**的创建报文,只给三个键(企业号 / 商户名 / 类别)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ 为什么不用 {@link #createBody(String, String)} 那个 DTO 夹具:那条路是**直调**
|
|
|
|
|
+ * {@code controller.create(dto)},压根不经过 Jackson —— 而本节要钉的正是「Jackson 怎么解
|
|
|
|
|
+ * 这个字段」。所以这里只能拼字符串、走 MockMvc。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String createBodyWithCategory(String categoryValue) {
|
|
|
|
|
+ return "{\"enterprise_id\":\"E1\",\"merchant_name\":\"示例商户\",\"category\":\""
|
|
|
|
|
+ + categoryValue + "\"}";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 让 {@code createMerchant} 过得了第一步的凭据闸 ——
|
|
|
|
|
+ * {@code LeshouyingProperties.missingOnboardingCredentials()} 的**五项**,缺一个都过不去。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>★ 这里给的是**编出来的占位值**(与 {@code LsyOnboardingServiceTest} 的 {@code CFG_*} 同形),
|
|
|
|
|
+ * 不是任何真实凭据;凭据不进仓库(Global Constraint 3)。
|
|
|
|
|
+ * <p>★ 为什么需要它:不设这五项的话,{@code createMerchant} 在**凭据闸**就抛 400 了,
|
|
|
|
|
+ * 根本走不到必填矩阵 —— 那样 {@code category_garbageValue_is400WithTheChineseLabel_notServerError}
|
|
|
|
|
+ * 里那个「商户类型」标签永远出不来,用例会以一条与它要钉的东西无关的理由红。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static void withOnboardingCredentials(LeshouyingProperties props) {
|
|
|
|
|
+ props.setChannelId("1001");
|
|
|
|
|
+ props.setBdId("2002");
|
|
|
|
|
+ props.setChannelType("3");
|
|
|
|
|
+ props.setRateCode("RT0045");
|
|
|
|
|
+ props.setOnboardingNotifyUrl("https://example.com/xjz/notify/lsy/onboarding");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static LsyCommitMerchantInfoDTO commitBody(String mchNo) {
|
|
private static LsyCommitMerchantInfoDTO commitBody(String mchNo) {
|
|
|
LsyCommitMerchantInfoDTO d = new LsyCommitMerchantInfoDTO();
|
|
LsyCommitMerchantInfoDTO d = new LsyCommitMerchantInfoDTO();
|
|
|
d.setMchNo(mchNo);
|
|
d.setMchNo(mchNo);
|