|
@@ -951,4 +951,87 @@ class HarOrderServiceTest {
|
|
|
assertTrue(e.getMessage().contains("至少填一个"), "实测: " + e.getMessage());
|
|
assertTrue(e.getMessage().contains("至少填一个"), "实测: " + e.getMessage());
|
|
|
verify(orderMapper, never()).selectOne(any());
|
|
verify(orderMapper, never()).selectOne(any());
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 管理端退款(按 id) ====================
|
|
|
|
|
+ //
|
|
|
|
|
+ // 后台是从列表点进来的,手上就有 id ⇒ 寻址走主键,不让调用方回填单号。
|
|
|
|
|
+ // 退款的实质动作与开放 API **共用同一条路径**(HarOrderService#doRefund):
|
|
|
|
|
+ // 抢占、守卫、回退、原因落库只写一份,两处不会漂移。
|
|
|
|
|
+
|
|
|
|
|
+ /** 按 id 取到的那一行就是被退的那一笔;调用方填的原因原样到上游 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void refundById_refundsTheRowTheIdPointsAt_withCallerReason() {
|
|
|
|
|
+ when(orderMapper.selectById(ORDER_ID)).thenReturn(paidOrder());
|
|
|
|
|
+ when(orderMapper.update(any(), any())).thenReturn(1);
|
|
|
|
|
+ when(harClient.refund(any())).thenReturn(response("10000", null));
|
|
|
|
|
+
|
|
|
|
|
+ service.refundById(ORDER_ID, "用户申请退款");
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<HarRefundRequest> req = ArgumentCaptor.forClass(HarRefundRequest.class);
|
|
|
|
|
+ verify(harClient).refund(req.capture());
|
|
|
|
|
+ assertEquals("用户申请退款", req.getValue().getReason());
|
|
|
|
|
+ assertEquals("HAR1", req.getValue().getThirdOrderNo(),
|
|
|
|
|
+ "退的必须是 id 指的那一笔。实测: " + req.getValue().getThirdOrderNo());
|
|
|
|
|
+ // 拿到的行已经在手上,不该再按单号查一次 —— 两次查询之间隔着一个「查到的还是不是这一行」的疑问
|
|
|
|
|
+ verify(orderMapper, never()).selectOne(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * id 不存在 ⇒ 与开放 API 同一句话「订单不存在」,且**一个上游字节都不发**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 这里用 verifyNoInteractions 而不是只 verify(never()).refund:后者会漏掉
|
|
|
|
|
+ * 将来在这个方法里新加的任何上游调用(查询、回调…)。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void refundById_unknownId_throwsAndNeverTouchesUpstream() {
|
|
|
|
|
+ when(orderMapper.selectById(ORDER_ID)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
|
|
+ () -> service.refundById(ORDER_ID, "用户申请退款"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("订单不存在", e.getMessage());
|
|
|
|
|
+ verify(orderMapper, never()).update(any(), any());
|
|
|
|
|
+ verifyNoInteractions(harClient);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 状态闸不能被「按 id 进来」这条新路绕过去:非「1 支付成功」一律不受理 */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void refundById_nonPaidOrder_rejectedWithoutTouchingUpstream() {
|
|
|
|
|
+ HarOrderEntity o = paidOrder();
|
|
|
|
|
+ o.setTradeStatus(HarOrderStatus.REFUNDED);
|
|
|
|
|
+ when(orderMapper.selectById(ORDER_ID)).thenReturn(o);
|
|
|
|
|
+
|
|
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
|
|
+ () -> service.refundById(ORDER_ID, null));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(e.getMessage().contains("不可退款"), "实测: " + e.getMessage());
|
|
|
|
|
+ verify(orderMapper, never()).update(any(), any());
|
|
|
|
|
+ verifyNoInteractions(harClient);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 留空 ⇒ 与开放 API **同一套**默认值,且发出去的那句与落库的那句是同一个串。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 上游对空 reason 是整笔直接拒(2026-09-24 实测),所以「非空」只是及格线;
|
|
|
|
|
+ * 这一条真正钉的是「页面上看到的原因 == 上游账务里收到的那句」。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void refundById_blankReason_defaultsToTheSameStringUpstreamAndInDb() {
|
|
|
|
|
+ when(orderMapper.selectById(ORDER_ID)).thenReturn(paidOrder());
|
|
|
|
|
+ when(orderMapper.update(any(), any())).thenReturn(1);
|
|
|
|
|
+ when(harClient.refund(any())).thenReturn(response("10000", null));
|
|
|
|
|
+
|
|
|
|
|
+ service.refundById(ORDER_ID, " ");
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<HarRefundRequest> req = ArgumentCaptor.forClass(HarRefundRequest.class);
|
|
|
|
|
+ verify(harClient).refund(req.capture());
|
|
|
|
|
+ ArgumentCaptor<HarOrderEntity> patch = ArgumentCaptor.forClass(HarOrderEntity.class);
|
|
|
|
|
+ verify(orderMapper).update(patch.capture(), any());
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(StrUtil.isNotBlank(req.getValue().getReason()),
|
|
|
|
|
+ "上游要求非空,留空必须补默认值。实测: " + req.getValue().getReason());
|
|
|
|
|
+ assertEquals(req.getValue().getReason(), patch.getValue().getRefundReason(),
|
|
|
|
|
+ "发出去的与落库的必须是同一个串。上游收到: " + req.getValue().getReason()
|
|
|
|
|
+ + ",落库: " + patch.getValue().getRefundReason());
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|