package com.talentcard.server.business.payment; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verifyNoInteractions; import java.math.BigDecimal; import java.util.Map; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.order.OrderRefundRequest; import com.talentcard.server.business.payment.huifu.HuifuRefundGateway; import com.talentcard.server.business.portal.AppOrderCreateRequest; import com.talentcard.server.business.portal.AppOrderPortalService; import com.talentcard.server.business.portal.PendingUsePortalService; import com.talentcard.server.business.portal.PendingUseRefundRequest; import com.talentcard.server.business.portal.PortalService; import com.talentcard.server.business.portal.UsageRecordPortalService; import com.talentcard.server.common.BusinessException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; /** * Seam: 一张没有实付金额的订单(整单被权益免费额度覆盖)到底能不能退。 * *
它没有钱在通道里——下单不经过支付,订单出生就是待核销状态——所以要退的是那一次被占用的免费额度,
* 而不是钱。这条用例走真实链路:商品配好免费额度 → 人才端下单(价格被算成 0)→ 使用记录里点退款,
* 断言额度确实回来了,并且从头到尾没有向通道发起过退款(那张单在通道侧根本不存在)。
*/
@SpringBootTest
@ActiveProfiles("test")
class ZeroAmountRefundTests {
@Autowired
private JdbcTemplate jdbc;
@Autowired
private AppOrderPortalService orderPortalService;
@Autowired
private PendingUsePortalService pendingUsePortalService;
@Autowired
private PortalService portalService;
@Autowired
private UsageRecordPortalService usageRecordPortalService;
@Autowired
private HuifuRefundService refundService;
@MockitoBean
private HuifuRefundGateway refundGateway;
private Long talentId;
private Long merchantId;
private Long productId;
private String suffix;
@BeforeEach
void prepareData() {
suffix = String.valueOf(System.nanoTime());
jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)",
"zero-refund-openid-" + suffix, "0元订单用户", "13900000007");
talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
jdbc.update("""
insert into talent_card(talent_id, card_no, card_level, card_type, status, deleted)
values (?, ?, '卓越人才卡', 'PERSONAL', 1, 0)
""", talentId, "ZC" + suffix);
jdbc.update("insert into merchant(merchant_name, contact_mobile, address, status, deleted) values (?, ?, ?, 1, 0)",
"0元订单商户 " + suffix, "08120000006", "Test address");
merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
// 免费权益:这个卡等级每单免费、周期内 1 次。resetCycle = NONE 表示不重置,额度窗口取全部历史,
// 于是「用掉没还回来」在测试里是长期可见的,不受运行日期影响。
jdbc.update("""
insert into merchant_product(merchant_id, product_no, product_type, product_name, original_price,
sale_price, stock, audit_status, status, rights_config, deleted)
values (?, ?, 'GROUP_BUY', '0元订单测试商品', 20.00, 20.00, -1, 2, 1, ?, 0)
""", merchantId, "ZERO-PRODUCT-" + suffix,
"[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"resetCycle\":\"NONE\"}]");
productId = jdbc.queryForObject("select max(id) from merchant_product", Long.class);
}
@Test
void refundingAZeroAmountOrderGivesTheFreeQuotaBackWithoutTouchingTheProvider() {
assertThat(talentPrice()).as("构造前提:额度在手,这个商品对这位人才是免费的").isEqualByComparingTo("0");
String orderNo = zeroAmountOrder();
assertThat(talentPrice()).as("下单即占用额度(0 元单没有支付这一步,出生就是待核销)").isEqualByComparingTo("20.00");
Map