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.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.time.LocalDateTime; import java.util.Map; import com.talentcard.server.business.merchant.MerchantRevokeVerificationRequest; import com.talentcard.server.business.merchant.MerchantWorkService; import com.talentcard.server.business.merchant.VerifyRequest; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; import com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway; 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; /** * 撤销核销的资金侧:钱必须真的回到通道延迟户,订单才允许翻回「待核销」。 * *

核销时钱已经从延迟户划给商户(进了基本户),撤销核销要靠通道的交易确认退款把它退回来。 * 回退没成功就不许翻单——否则订单显示「待核销」,钱却已经结算给商户,商户还能拿着这张单再核销一次; * 等 D1 自动结算把钱划到商户银行卡之后,通道那边也退不回来了。 */ @SpringBootTest @ActiveProfiles("test") class RevokeVerificationFundTests { private static final String HUIFU_ID = "6666000226048687"; @Autowired private JdbcTemplate jdbc; @Autowired private MerchantWorkService merchantWorkService; @MockitoBean private HuifuDelayAccountGateway delayGateway; private Long merchantId; private Long storeId; private Long talentId; private MerchantPrincipal principal; @BeforeEach void prepareMerchant() { String suffix = String.valueOf(System.nanoTime()); jdbc.update("insert into merchant(merchant_name, status, can_login_merchant_app, deleted) values (?, 1, 1, 0)", "撤销核销商户 " + suffix); merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class); jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", merchantId, "撤销核销门店 " + suffix); storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class); jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)", "revoke-fund-" + suffix, "撤销核销用户", "13900000010"); talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class); principal = new MerchantPrincipal(9101L, merchantId, storeId, "operator"); } /** 撤销核销要把钱退回延迟户:退回成功才翻单,这笔钱才算重新变成不可结算。 */ @Test void revokesOnlyAfterTheMoneyComesBackToTheDelayAccount() { long orderId = verifiedOrderWithConfirmedMoney(); when(delayGateway.refund(any())).thenReturn(revert(HuifuDelayAccountGateway.HuifuDelayRevert.Outcome.SUCCEEDED, "00000000", "交易成功")); Map revoked = merchantWorkService.revokeVerification(principal, new MerchantRevokeVerificationRequest(String.valueOf(orderId), "test reversal"), "revoke-back-" + orderId); assertThat(revoked).containsEntry("orderStatus", "pending_verification"); assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, orderId)) .isZero(); // 钱回到延迟户,商户下次核销时会走一次全新的交易确认。 assertThat(fundsStatus(orderId)).isEqualTo("pending"); } /** * 通道没把钱退回来就不许翻单:否则订单显示「待核销」,钱却已经结算给商户,商户还能拿着这张单再核销。 */ @Test void refusesToRevokeWhileTheMoneyHasNotComeBack() { long orderId = verifiedOrderWithConfirmedMoney(); when(delayGateway.refund(any())).thenReturn(revert( HuifuDelayAccountGateway.HuifuDelayRevert.Outcome.NEEDS_REVIEW, "23000003", "已无已确认金额")); assertThatThrownBy(() -> merchantWorkService.revokeVerification(principal, new MerchantRevokeVerificationRequest(String.valueOf(orderId), "test reversal"), "revoke-held-" + orderId)) .isInstanceOf(BusinessException.class) // 语义未定时不能报成「已结算」,也不能让商户反复重试:请他联系平台。 .hasMessageContaining("无法核对"); assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, orderId)) .isEqualTo(1); // 钱的位置没定:状态停在 reverting,等人工核对,不能当成已经退回延迟户。 assertThat(fundsStatus(orderId)).isEqualTo("reverting"); } /** 钱已经结算到银行卡时,商户要知道原因,而不是只看到一句「撤销失败」。 */ @Test void tellsTheMerchantWhenTheSettledMoneyCannotBeCalledBack() { long orderId = verifiedOrderWithConfirmedMoney(); when(delayGateway.refund(any())).thenReturn(revert( HuifuDelayAccountGateway.HuifuDelayRevert.Outcome.FAILED, "22000003", "账户余额不足")); assertThatThrownBy(() -> merchantWorkService.revokeVerification(principal, new MerchantRevokeVerificationRequest(String.valueOf(orderId), "test reversal"), "revoke-settled-" + orderId)) .isInstanceOf(BusinessException.class) .hasMessageContaining("已结算到商户银行卡"); assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, orderId)) .isEqualTo(1); // 明确拒绝:钱没有动,状态回到可重试的 confirmed,商户补足后还能再撤。 assertThat(fundsStatus(orderId)).isEqualTo("confirmed"); } private HuifuDelayAccountGateway.HuifuDelayRevert revert( HuifuDelayAccountGateway.HuifuDelayRevert.Outcome outcome, String code, String message) { return new HuifuDelayAccountGateway.HuifuDelayRevert(outcome, code, message, "HF-R-" + System.nanoTime()); } /** * 造一笔「已核销 + 资金已确认给商户」的延迟入账订单,返回订单主键。 * *

核销走真实服务:它会写下核销记录并调用交易确认,撤销时才有核销记录可撤、有确认可退。 */ private long verifiedOrderWithConfirmedMoney() { String orderNo = "ORDER-REVOKE-" + System.nanoTime(); jdbc.update(""" insert into benefit_order(order_no, talent_id, merchant_id, store_id, order_type, order_amount, subsidy_amount, pay_amount, order_status, verify_status, created_at, deleted) values (?, ?, ?, ?, 'GROUP_BUY', 10000, 1000, 9000, 1, 0, ?, 0) """, orderNo, talentId, merchantId, storeId, java.sql.Timestamp.valueOf(LocalDateTime.now())); Long orderId = jdbc.queryForObject("select id from benefit_order where order_no = ?", Long.class, orderNo); jdbc.update(""" insert into payment_transaction(payment_no, provider, order_id, order_no, talent_id, merchant_id, provider_huifu_id, amount, status, idempotency_key, attempt_no, paid_at, funds_confirmation_status) values (?, 'HUIFU', ?, ?, ?, ?, ?, 9000, 'SUCCEEDED', ?, 1, now(), 'pending') """, "P-VERIFY-" + System.nanoTime(), orderId, orderNo, talentId, merchantId, HUIFU_ID, "verify-payment-" + orderNo); when(delayGateway.confirm(any())).thenReturn(new HuifuDelayAccountGateway.HuifuDelayConfirmation( HuifuDelayAccountGateway.HuifuDelayConfirmation.Outcome.SUCCEEDED, "00000000", "交易成功", "0.00", "90.00", "HF-C-" + System.nanoTime())); merchantWorkService.verify(orderNo, principal, new VerifyRequest("merchant", "test verification"), "verify-" + orderNo); return orderId; } private String fundsStatus(long orderId) { return jdbc.queryForObject("select funds_confirmation_status from payment_transaction where order_id = ?", String.class, orderId); } }