|
|
@@ -706,6 +706,117 @@ class LsyRefundServiceTest {
|
|
|
verify(callbackService, never()).notifyOrder(any());
|
|
|
}
|
|
|
|
|
|
+ // ---- 实退金额(refund_settle):唯一来源是**退款查询**那条腿 ----
|
|
|
+ //
|
|
|
+ // 【用户报障 2026-09-30】「退款记录 实退(元) 无数据」。根因不是脏数据,是**读错了腿**:
|
|
|
+ // · `createRefund` 从**退款接口** `/v3/refundorder` 的响应里取 `refund_settle`(:259),
|
|
|
+ // 而该接口的响应字段表(showdoc 196236527)**没有这个字段** ⇒ 取到的恒是 null,
|
|
|
+ // 再被 MP 的 NOT_NULL 策略静默跳过 ⇒ 这一列自建表起**从来没有过值**。
|
|
|
+ // · 真正会回它的那条腿 `/v3/queryrefund`(退款查询),在 `settleFromQuery` 里
|
|
|
+ // **只写状态**,把它丢掉了。
|
|
|
+ // 【实证,不是推断】`logs/_lsy-refund-query-probe.txt`(2026-09-28 真打上游)记着
|
|
|
+ // `/v3/queryrefund` 的真实键集,里面**有** `refund_settle`、**没有** `refund_fee`。
|
|
|
+ // 【为什么上面那条 `refund_上游响应里的字段_补写到预插入的那一行` 一直是绿的】
|
|
|
+ // 因为本类的 `resp()` 夹具**自己往退款接口响应里塞了** `refund_settle`(见该辅助方法)
|
|
|
+ // —— 上游从不这么发。夹具造出了一个现实中不存在的报文形状。
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 正控:退款查询确认成功且回传 `refund_settle` ⇒ 必须落库。
|
|
|
+ *
|
|
|
+ * <p>没有这条,「实退列永远为空」这个缺陷在单测里**看不见**。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void settleFromQuery_退款查询带refund_settle_落库() {
|
|
|
+ LsyRefundEntity refund = refund("REFUNDING");
|
|
|
+ refund.setOrderId(1L);
|
|
|
+ LsyOrderEntity o = order();
|
|
|
+ when(refundMapper.update(any(), any())).thenReturn(1);
|
|
|
+ when(orderMapper.selectById(1L)).thenReturn(o);
|
|
|
+ when(orderService.applyRefundResult(eq(o), eq(30L), anyString())).thenReturn(true);
|
|
|
+
|
|
|
+ Map<String, Object> r = resp("PAY_SUCCESS");
|
|
|
+ r.put("refund_settle", "97");
|
|
|
+ service.settleFromQuery(refund, r);
|
|
|
+
|
|
|
+ var captor = org.mockito.ArgumentCaptor.forClass(LsyRefundEntity.class);
|
|
|
+ verify(refundMapper, atLeastOnce()).update(captor.capture(), any());
|
|
|
+ assertTrue(captor.getAllValues().stream()
|
|
|
+ .anyMatch(x -> Long.valueOf(97L).equals(x.getRefundSettle())),
|
|
|
+ "退款查询回传的实退金额必须落库 —— 它是 refund_settle 的唯一来源");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上游没回 `refund_settle` 时**不许编一个数字**。
|
|
|
+ *
|
|
|
+ * <p>该字段在文档里是可空项(必填列留空),所以「缺席」是常态而不是异常。
|
|
|
+ * 编一个等于把「不知道」写成「就是这么多」—— 对账时看不出来。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void settleFromQuery_退款查询没带refund_settle_不写这一列() {
|
|
|
+ LsyRefundEntity refund = refund("REFUNDING");
|
|
|
+ refund.setOrderId(1L);
|
|
|
+ LsyOrderEntity o = order();
|
|
|
+ when(refundMapper.update(any(), any())).thenReturn(1);
|
|
|
+ when(orderMapper.selectById(1L)).thenReturn(o);
|
|
|
+ when(orderService.applyRefundResult(eq(o), eq(30L), anyString())).thenReturn(true);
|
|
|
+
|
|
|
+ Map<String, Object> r = resp("PAY_SUCCESS");
|
|
|
+ r.remove("refund_settle");
|
|
|
+ service.settleFromQuery(refund, r);
|
|
|
+
|
|
|
+ var captor = org.mockito.ArgumentCaptor.forClass(LsyRefundEntity.class);
|
|
|
+ // 正控:这一轮**确实写了库**(状态那一次),否则下面的 allMatch 是空集上恒真。
|
|
|
+ verify(refundMapper, atLeastOnce()).update(captor.capture(), any());
|
|
|
+ assertTrue(captor.getAllValues().stream().allMatch(x -> x.getRefundSettle() == null),
|
|
|
+ "响应里没有这个字段时不得编一个数字 —— 只能留空等下一轮");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在途 / 未知码两支**仍然一次库都不写**(含 `refund_settle`)。
|
|
|
+ *
|
|
|
+ * <p>这两支的「不写库」是既有不变量,由上面 `settleFromQuery_退款申请中_不落状态返回null`
|
|
|
+ * 与 `settleFromQuery_未知码_不落任何状态` 的 `never()` 钉着。本条是**它们的加固版**:
|
|
|
+ * 那两条用的夹具里 `refund_settle` 是**有值**的(`resp()` 塞了 "100"),
|
|
|
+ * 所以只要有人把实退写入挪到分支分发之前,它们就会红 —— 这正是想要的报警。
|
|
|
+ * 这里把它写成显式断言,是为了让「为什么它们能挡住这次改动」不必靠读者自己发现。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void settleFromQuery_在途与未知码_连实退也不写() {
|
|
|
+ LsyRefundEntity a = refund("REFUNDING");
|
|
|
+ LsyRefundEntity b = refund("REFUNDING");
|
|
|
+
|
|
|
+ assertNull(service.settleFromQuery(a, resp("PAY_IN_PROCESS")));
|
|
|
+ assertNull(service.settleFromQuery(b, resp("SOME_UNKNOWN_CODE")));
|
|
|
+
|
|
|
+ verify(refundMapper, never()).update(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 终态行补写实退:**只写实退,不碰状态**。
|
|
|
+ *
|
|
|
+ * <p>这是同步成功 / 回调先到那两类行的唯一补写通道 —— 它们的状态行已终态,
|
|
|
+ * 对账腿的 PENDING 谓词扫不到它们(见 `LsyReconcileScheduler#backfillRefundSettle`)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void backfillSettle_只写实退不碰状态() {
|
|
|
+ LsyRefundEntity refund = refund("SUCCESS");
|
|
|
+ refund.setOrderId(1L);
|
|
|
+ refund.setRefundNo("R001");
|
|
|
+ when(client.post(eq("/v3/queryrefund"), anyMap()))
|
|
|
+ .thenReturn(Map.of("return_code", "SUCCESS", "result_code", "PAY_SUCCESS",
|
|
|
+ "refund_settle", "97"));
|
|
|
+ when(orderMapper.selectById(1L)).thenReturn(order());
|
|
|
+ when(refundMapper.update(any(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ service.backfillSettle(refund);
|
|
|
+
|
|
|
+ var captor = org.mockito.ArgumentCaptor.forClass(LsyRefundEntity.class);
|
|
|
+ verify(refundMapper).update(captor.capture(), any());
|
|
|
+ LsyRefundEntity patch = captor.getValue();
|
|
|
+ assertEquals(97L, patch.getRefundSettle(), "必须落上游回传的实退金额");
|
|
|
+ assertNull(patch.getStatus(), "补写实退不得顺手写状态 —— 终态行的状态不可回写");
|
|
|
+ }
|
|
|
+
|
|
|
// ---- applyNotifyResult:退款行的幂等闸门(Task 12 依赖) ----
|
|
|
|
|
|
@Test
|