|
|
@@ -8,6 +8,7 @@ import com.payment.platform.module.payment.leshouying.entity.LsyOrderEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyRefundEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyOrderMapper;
|
|
|
import com.payment.platform.module.payment.leshouying.mapper.LsyRefundMapper;
|
|
|
+import com.payment.platform.module.payment.stream.OrderEventPublisher;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
@@ -26,11 +27,18 @@ import static org.mockito.Mockito.*;
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
|
class LsyRefundServiceTest {
|
|
|
|
|
|
+ /**
|
|
|
+ * 夹具租户 —— 订单/退款两个夹具都挂它,好让「发布用的是实体自己的租户」这条断言
|
|
|
+ * 有**非 null 且唯一**的值可比:都留 null 的话,一个恒发 {@code null} 的实现也会绿。
|
|
|
+ */
|
|
|
+ private static final Long TENANT = 7L;
|
|
|
+
|
|
|
@Mock private LeshouyingClient client;
|
|
|
@Mock private LsyRefundMapper refundMapper;
|
|
|
@Mock private LsyOrderMapper orderMapper;
|
|
|
@Mock private LsyOrderService orderService;
|
|
|
@Mock private LsyCallbackService callbackService;
|
|
|
+ @Mock private OrderEventPublisher publisher;
|
|
|
private LsyRefundService service;
|
|
|
/** 只为对账游标的初值(`reconcileDelaySeconds`)而需要;用默认值即可。 */
|
|
|
private final LeshouyingProperties props = new LeshouyingProperties();
|
|
|
@@ -38,12 +46,13 @@ class LsyRefundServiceTest {
|
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
|
service = new LsyRefundService(client, new LeshouyingResponseParser(), refundMapper,
|
|
|
- orderMapper, orderService, callbackService, props);
|
|
|
+ orderMapper, orderService, callbackService, props, publisher);
|
|
|
}
|
|
|
|
|
|
private LsyOrderEntity order() {
|
|
|
LsyOrderEntity o = new LsyOrderEntity();
|
|
|
o.setId(1L);
|
|
|
+ o.setTenantId(TENANT);
|
|
|
o.setOutTradeNo("OUT001");
|
|
|
o.setMchNo("Y52211090004");
|
|
|
o.setPayTraceNo("PT001");
|
|
|
@@ -261,6 +270,12 @@ class LsyRefundServiceTest {
|
|
|
org.mockito.InOrder io = inOrder(refundMapper, client);
|
|
|
io.verify(refundMapper).insert(any());
|
|
|
io.verify(client).post(eq("/v3/refundorder"), anyMap());
|
|
|
+
|
|
|
+ // 订单实时信号:本路径上 pay_lsy_refund **真的写了 4 次**,所以恰好 4 条信号 ——
|
|
|
+ // 预插入(insert) ①、补身份(persistUpstreamIdentity) ②、CREATED→REFUNDING ④、
|
|
|
+ // REFUNDING→SUCCESS ④。用精确次数而不是 atLeastOnce:后者在「后面的写点会补发」
|
|
|
+ // 之下**恒真**,删掉任何一条发布行它都不会红(这正是本任务要钉的东西)。
|
|
|
+ verify(publisher, times(4)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -337,6 +352,10 @@ class LsyRefundServiceTest {
|
|
|
// 状态推进也必须真的发生(CREATED → REFUNDING)。
|
|
|
assertTrue(captor.getAllValues().stream().anyMatch(x -> "REFUNDING".equals(x.getStatus())),
|
|
|
"收到响应 = 上游已受理 ⇒ 必须离开 CREATED");
|
|
|
+
|
|
|
+ // 订单实时信号:与上一条同路(同步成功),写点同样是 4 个 ⇒ 4 条信号,
|
|
|
+ // 其中**补身份那一次**(persistUpstreamIdentity)是本用例自己钉的那个写点。
|
|
|
+ verify(publisher, times(4)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -779,6 +798,11 @@ class LsyRefundServiceTest {
|
|
|
assertTrue(captor.getAllValues().stream()
|
|
|
.anyMatch(x -> Long.valueOf(97L).equals(x.getRefundSettle())),
|
|
|
"退款查询回传的实退金额必须落库 —— 它是 refund_settle 的唯一来源");
|
|
|
+
|
|
|
+ // 订单实时信号:本路径两个写点 —— 实退补写(persistRefundSettle) ③、状态收敛 ④
|
|
|
+ // ⇒ 恰好 2 条。写成 2 而不是 1,是因为①这个数就钉住了「实退那条也发了」:
|
|
|
+ // 删掉 persistRefundSettle 的发布行 → 只剩 1 条 → 红。
|
|
|
+ verify(publisher, times(2)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -805,6 +829,11 @@ class LsyRefundServiceTest {
|
|
|
verify(refundMapper, atLeastOnce()).update(captor.capture(), any());
|
|
|
assertTrue(captor.getAllValues().stream().allMatch(x -> x.getRefundSettle() == null),
|
|
|
"响应里没有这个字段时不得编一个数字 —— 只能留空等下一轮");
|
|
|
+
|
|
|
+ // 订单实时信号(负例的正控):实退缺席 ⇒ persistRefundSettle 早退、**一个字节都没写**
|
|
|
+ // ⇒ 本路径只剩状态收敛那一条信号。写成 1 而不是 never():状态那一次写是真的,
|
|
|
+ // 它该发。若有人把发布行挪到早退分支之上(或挪到 settle==null 之前),这里会是 2 → 红。
|
|
|
+ verify(publisher, times(1)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -862,6 +891,9 @@ class LsyRefundServiceTest {
|
|
|
|
|
|
assertTrue(service.applyNotifyResult(refund, "TRADE_REFUND", "成功", "SUCCESS"));
|
|
|
assertEquals("SUCCESS", refund.getStatus());
|
|
|
+ // 订单实时信号:真的流转了(受影响行数 = 1)⇒ 恰好一条。本方法只此一个写点,
|
|
|
+ // 所以这里的 times(1) 也就是「命中一行才发」的最小正例。
|
|
|
+ verify(publisher, times(1)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -908,6 +940,9 @@ class LsyRefundServiceTest {
|
|
|
|
|
|
assertFalse(service.applyNotifyResult(refund, "TRADE_REFUND", "成功", "SUCCESS"),
|
|
|
"重复回调必须返回 false,否则订单侧会重复累加 refunded_amount");
|
|
|
+ // 订单实时信号(负例):命中 0 行 = 一个字节都没改。发了就是让退款页白拉一次,
|
|
|
+ // 而且是在「什么都没变」的时候 —— 信号必须挂在守卫**之后**。
|
|
|
+ verifyNoInteractions(publisher);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1005,6 +1040,7 @@ class LsyRefundServiceTest {
|
|
|
private LsyRefundEntity refund(String status) {
|
|
|
LsyRefundEntity r = new LsyRefundEntity();
|
|
|
r.setId(9L);
|
|
|
+ r.setTenantId(TENANT);
|
|
|
r.setOutRefundNo("RF001");
|
|
|
r.setRefundFee(30L);
|
|
|
r.setStatus(status);
|