yeg
2026-09-17 e3f412c5474311a471345ea508db4875c9ec648e
talent-card-server/src/test/java/com/talentcard/server/business/payment/PendingPaymentConfirmationTests.java
@@ -5,11 +5,14 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import com.talentcard.server.business.payment.PaymentReconciliationService.PendingPaymentConfirmation;
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentGateway;
import com.talentcard.server.business.payment.huifu.HuifuPaymentCloseCommand;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
@@ -61,7 +64,7 @@
    void prepareData() {
        suffix = String.valueOf(System.nanoTime());
        // 共享测试库里可能留着历史遗留的挂起流水,只有本用例自己创建的才能落进对账窗口。
        jdbc.update("update payment_transaction set created_at = now() where status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CANCEL_UNCONFIRMED')");
        jdbc.update("update payment_transaction set created_at = now(), last_reconciled_at = null where status in ('CREATED', 'PREPAY_CREATED', 'CREATE_UNKNOWN', 'CLOSING', 'CANCEL_UNCONFIRMED')");
        jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)",
                "pending-payment-openid-" + suffix, "Pending payment user", "13900000003");
        talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
@@ -112,27 +115,6 @@
    }
    @Test
    void cancellingIsRefusedAndRecordedWhenClosingRevealsTheTradeWasAlreadyPaid() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        // 查单说「交易不存在」,关单却回「原订单已为终态」且原交易已成功:钱可能真的在,不能当成没收过款。
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "resp_code", "23000001", "resp_desc", "交易不存在", "huifu_id", huifuId)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "F", "resp_code", "23000000", "resp_desc", "原订单已为终态,请发起查询交易获取",
                "org_trans_stat", "S", "huifu_id", huifuId)));
        // 查单拿不到金额,无法把订单落成已支付,所以不能谎称已支付;但订单也绝不能被取消。
        assertThatThrownBy(() -> pendingOrderService.cancel(orderNo, talentId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("确认中");
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo)).isZero();
        assertThat(count("select count(*) from payment_transaction where payment_no = ? and status = 'PREPAY_CREATED'", paymentNo)).isEqualTo(1);
        assertThat(openIssueCount("PAYMENT_CLOSE_CONFLICT", paymentNo)).isEqualTo(1L);
    }
    @Test
    void cancellingProceedsButKeepsThePrepayWatchableWhenTheProviderRefusesToCloseIt() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
@@ -151,32 +133,107 @@
    }
    @Test
    void cancellingIsRefusedWhileTheProviderIsStillClosingThePrepay() {
    void cancellingClosesThePrepayEvenWhenTheQueryOnlySaysItIsProcessing() {
        String orderNo = pendingOrder();
        pendingPayment(orderNo, 2000L);
        String paymentNo = pendingPayment(orderNo, 2000L);
        // 查单只答「处理中」:旧实现把它当成关单的前置门,于是关单一次都不会被调用。
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "P", "resp_code", "00000100", "resp_desc", "交易处理中", "trans_amt", "20.00", "huifu_id", huifuId)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "F", "resp_code", "20000001", "resp_desc", "不允许关闭一分钟以内的订单", "huifu_id", huifuId)));
        Map<String, Object> result = pendingOrderService.cancel(orderNo, talentId);
        verify(paymentGateway, times(1)).closeMiniProgramPayment(any());
        assertThat(((Number) result.get("orderStatus")).intValue())
                .as("关单回执是明确拒绝,不是「还在处理中」,因此允许取消并继续盯防")
                .isEqualTo(2);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("CANCEL_UNCONFIRMED");
    }
    @Test
    void closingReportsTheOriginalTradeSucceededAndTheQueryThenLandsTheOrderAsPaid() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        // 第一次查单说交易不存在(所以会去关单),关单回执说原交易已经成功,再查一次拿到了金额。
        when(paymentGateway.queryMiniProgramPayment(any()))
                .thenReturn(providerResponse(Map.of("resp_code", "23000001", "resp_desc", "交易不存在", "huifu_id", huifuId)))
                .thenReturn(providerResponse(Map.of(
                        "trans_stat", "S", "resp_code", "00000000", "trans_amt", "20.00",
                        "huifu_id", huifuId, "org_hf_seq_id", "HF-CLOSE-PAID-" + suffix)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "F", "resp_code", "23000000", "resp_desc", "原订单已为终态,请发起查询交易获取",
                "org_trans_stat", "S", "huifu_id", huifuId)));
        assertThatThrownBy(() -> pendingOrderService.cancel(orderNo, talentId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("已支付");
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo))
                .as("关单回执说原交易已成功,就必须落成已支付,而不是把一笔真钱留在待支付里")
                .isEqualTo(1);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("SUCCEEDED");
    }
    @Test
    void cancellingWaitsForTheCloseConclusionWhenClosingRevealsTheTradeWasAlreadyPaid() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        // 查单说「交易不存在」,关单却回「原订单已为终态」且原交易已成功:钱可能真的在,不能当成没收过款。
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "resp_code", "23000001", "resp_desc", "交易不存在", "huifu_id", huifuId)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "F", "resp_code", "23000000", "resp_desc", "原订单已为终态,请发起查询交易获取",
                "org_trans_stat", "S", "huifu_id", huifuId)));
        // 查单拿不到金额,无法把订单落成已支付,所以不能谎称已支付;但订单也绝不能被取消。
        Map<String, Object> result = pendingOrderService.cancel(orderNo, talentId);
        assertThat(result)
                .as("关单没有结论时,取消的意图被受理但要等结论,而不是给一个永久报错")
                .containsEntry("cancellationPending", true);
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo)).isZero();
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .as("关单中:既不能当已关闭(通道还可能收钱),也不该是终点")
                .isEqualTo("CLOSING");
        assertThat(count("select count(*) from payment_transaction where order_no = ?", orderNo))
                .as("关单中期间只能复用,不允许新建第二张可被支付的预支付单")
                .isEqualTo(1);
        assertThat(openIssueCount("PAYMENT_CLOSE_CONFLICT", paymentNo)).isEqualTo(1L);
    }
    @Test
    void cancellingWaitsForTheCloseConclusionWhileTheProviderIsStillClosingThePrepay() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "I", "resp_code", "00000000", "trans_amt", "20.00", "huifu_id", huifuId)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "P", "resp_code", "00000100", "resp_desc", "交易处理中", "huifu_id", huifuId)));
        assertThatThrownBy(() -> pendingOrderService.cancel(orderNo, talentId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("确认中");
        Map<String, Object> result = pendingOrderService.cancel(orderNo, talentId);
        assertThat(result).containsEntry("cancellationPending", true);
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo)).isZero();
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("CLOSING");
        assertThat(jdbc.queryForObject("select close_attempts from payment_transaction where payment_no = ?", Integer.class, paymentNo))
                .as("关单重试次数要落库,否则重试上限永远到不了")
                .isEqualTo(1);
    }
    @Test
    void cancellingIsRefusedWhenTheProviderCannotSayWhetherTheMoneyWasCollected() {
    void cancellingWaitsForTheProviderWhenItCannotSayWhetherTheMoneyWasCollected() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        when(paymentGateway.queryMiniProgramPayment(any()))
                .thenThrow(new HuifuPaymentGatewayException("Huifu payment query failed", true, null));
        assertThatThrownBy(() -> pendingOrderService.cancel(orderNo, talentId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("确认中");
        Map<String, Object> result = pendingOrderService.cancel(orderNo, talentId);
        assertThat(result).containsEntry("cancellationPending", true);
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo)).isZero();
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .as("an unanswerable payment question must not destroy the only record of the money")
@@ -202,18 +259,200 @@
    }
    @Test
    void retryingAPaymentIsRefusedWhenTheProviderCannotSayWhetherTheMoneyWasCollected() {
    void retryingAPaymentReportsWaitingWhenTheProviderCannotSayWhetherTheMoneyWasCollected() {
        String orderNo = pendingOrder();
        pendingPayment(orderNo, 2000L);
        when(paymentGateway.queryMiniProgramPayment(any()))
                .thenThrow(new HuifuPaymentGatewayException("Huifu payment query failed", true, null));
        assertThatThrownBy(() -> paymentService.beginMiniProgramPayment(orderNo, talentId, 1, "retry-key-" + suffix))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("确认中");
        Map<String, Object> result = paymentService.beginMiniProgramPayment(orderNo, talentId, 1, "retry-key-" + suffix);
        assertThat(result)
                .as("问不出来的不确定只属于这一次支付尝试,不该把用户堵在一句报错上")
                .containsEntry("paymentStatus", "PAYMENT_PROCESSING");
        verify(paymentGateway, never()).createMiniProgramPayment(any());
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo)).isZero();
    }
    @Test
    void askingToPayAgainReusesTheStoredPrepayWithoutAskingTheProvider() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L, "PREPAY_CREATED", "{\"appId\":\"wx-test\",\"timeStamp\":\"1\"}");
        Map<String, Object> result = paymentService.beginMiniProgramPayment(orderNo, talentId, 1, "reuse-key-" + suffix);
        assertThat(result)
                .as("本地已经持有可用的支付参数时直接把这张预支付单交回去——查通道只会得到「处理中」这种没有结论的答复")
                .containsEntry("paymentStatus", "PAYMENT_PENDING")
                .containsEntry("paymentNo", paymentNo);
        assertThat(result.get("paymentParams")).isNotNull();
        verify(paymentGateway, never()).queryMiniProgramPayment(any());
        verify(paymentGateway, never()).createMiniProgramPayment(any());
        assertThat(count("select count(*) from payment_transaction where order_no = ?", orderNo))
                .as("复用不新建支付流水")
                .isEqualTo(1);
    }
    @Test
    void askingToPayAgainIsRefusedWhenAnUndecidedPrepayCannotBeReused() {
        String orderNo = pendingOrder();
        // 先取消,通道答「处理中」→ 关单没有结论,这次尝试停在 CLOSING 且订单仍待支付;
        // 它没有留下可复用的支付参数,因此既不能复用、也不许新建。
        String paymentNo = pendingPayment(orderNo, 2000L);
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "I", "resp_code", "00000000", "trans_amt", "20.00", "huifu_id", huifuId)));
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "P", "resp_code", "00000100", "resp_desc", "交易处理中", "huifu_id", huifuId)));
        pendingOrderService.cancel(orderNo, talentId);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo)).isEqualTo("CLOSING");
        clearInvocations(paymentGateway);
        Map<String, Object> result = paymentService.beginMiniProgramPayment(orderNo, talentId, 1, "blocked-key-" + suffix);
        assertThat(result).containsEntry("paymentStatus", "PAYMENT_PROCESSING");
        verify(paymentGateway, never()).createMiniProgramPayment(any());
        assertThat(count("select count(*) from payment_transaction where order_no = ?", orderNo))
                .as("上一张预支付单没有被证明不可能再被支付之前,不允许给同一笔订单再建一张")
                .isEqualTo(1);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("CLOSING");
    }
    @Test
    void anExpiredOrderCannotBePaidEvenWhenAPrepayIsStillStoredLocally() {
        String orderNo = pendingOrder();
        pendingPayment(orderNo, 2000L, "PREPAY_CREATED", "{\"appId\":\"wx-test\",\"timeStamp\":\"1\"}");
        expireNow(orderNo);
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "resp_code", "23000001", "resp_desc", "交易不存在", "huifu_id", huifuId)));
        assertThatThrownBy(() -> paymentService.beginMiniProgramPayment(orderNo, talentId, 1, "expired-key-" + suffix))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("超时");
        verify(paymentGateway, never()).createMiniProgramPayment(any());
    }
    @Test
    void abandoningAPaymentOnlyClosesItAndNeverJudgesWhetherMoneyArrived() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        // 客户端说「我放弃了」,而关单回执说原交易已经成功——即使如此也不能据客户端的话去查单或结算。
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "F", "resp_code", "23000000", "resp_desc", "原订单已为终态,请发起查询交易获取",
                "org_trans_stat", "S", "huifu_id", huifuId)));
        PendingPaymentConfirmation confirmation = reconciliationService.abandonPendingPayments(orderNo, paymentNo);
        verify(paymentGateway, never()).queryMiniProgramPayment(any());
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .as("放弃只触发关单:关单没拿到「关掉」的结论就停在关单中,绝不做任何收款判定")
                .isEqualTo("CLOSING");
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo))
                .as("放弃支付不是取消订单")
                .isZero();
        assertThat(confirmation.collected()).isFalse();
        assertThat(openIssueCount("PAYMENT_CLOSE_CONFLICT", paymentNo)).isEqualTo(1L);
    }
    @Test
    void abandoningAPaymentClosesThePrepaySoTheOrderCannotBePaidTwice() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "S", "resp_code", "00000000", "resp_desc", "交易成功", "org_trans_stat", "P", "huifu_id", huifuId)));
        reconciliationService.abandonPendingPayments(orderNo, paymentNo);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("CLOSED");
    }
    @Test
    void thePrepayCloseStopsRetryingAtTheCapAndEndsTheOrderWithAWatcher() {
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "I", "resp_code", "00000000", "trans_amt", "20.00", "huifu_id", huifuId)));
        // 通道一直答「还在处理中」:关单永远没有结论。
        when(paymentGateway.closeMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "trans_stat", "P", "resp_code", "00000100", "resp_desc", "交易处理中", "huifu_id", huifuId)));
        assertThat(pendingOrderService.cancel(orderNo, talentId)).containsEntry("cancellationPending", true);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .isEqualTo("CLOSING");
        // 快车道每分钟一轮。每轮都把行推回窗口内(共享库里的残留行不影响本用例:断言只按自己的流水号过滤)。
        for (int round = 0; round < 3; round++) reconcileOnce(paymentNo);
        assertThat(jdbc.queryForObject("select close_attempts from payment_transaction where payment_no = ?", Integer.class, paymentNo))
                .as("关单重试有明确上限")
                .isEqualTo(3);
        assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                .as("到上限后落成「取消未确认」,预支付单继续被盯着")
                .isEqualTo("CANCEL_UNCONFIRMED");
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo))
                .as("关单中不能永远挂着:到上限时订单落成已取消,不留一笔既付不了也退不出的僵尸订单")
                .isEqualTo(2);
        assertThat(openIssueCount("PAYMENT_CLOSE_UNCONFIRMED", paymentNo))
                .as("放弃关单要留下问题记录给人工")
                .isEqualTo(1L);
        // 到上限后不再重试:再跑一轮,关单重试计数不再增长。
        reconcileOnce(paymentNo);
        assertThat(jdbc.queryForObject("select close_attempts from payment_transaction where payment_no = ?", Integer.class, paymentNo))
                .isEqualTo(3);
    }
    @Test
    void anExpiredOrderWithAClosingOrUnconfirmedAttemptIsNotCancelledByTheDeadline() {
        for (String status : List.of("CLOSING", "CANCEL_UNCONFIRMED")) {
            String orderNo = pendingOrder();
            String paymentNo = pendingPayment(orderNo, 2000L, status);
            expireNow(orderNo);
            pendingOrderService.expireDueOrders(talentId);
            assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo))
                    .as("%s 的预支付单仍可能被支付,自动取消会把已收下的钱变成没有出口的悬账", status)
                    .isZero();
            pendingOrderService.detail(orderNo, talentId);
            assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, orderNo))
                    .as("单个订单的到期路径同样不能取消它(%s)", status)
                    .isZero();
            assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo))
                    .isEqualTo(status);
        }
    }
    @Test
    void theReconciliationRotatesInsteadOfAlwaysTakingTheOldestHundredUndecidedPayments() {
        String backlogOrderNo = pendingOrder();
        Long backlogOrderId = jdbc.queryForObject("select id from benefit_order where order_no = ?", Long.class, backlogOrderNo);
        // 100 条刚被处理过的历史未决流水:它们都在退避窗口内,不该再被选中。
        for (int index = 0; index < 100; index++) {
            jdbc.update("""
                    insert into payment_transaction(payment_no, provider, order_id, order_no, talent_id, merchant_id,
                                                    provider_huifu_id, amount, status, idempotency_key, created_at, last_reconciled_at)
                    values (?, 'HUIFU', ?, ?, ?, ?, ?, 2000, 'PREPAY_CREATED', ?, now() - interval 30 minute, now())
                    """, "P-BACKLOG-" + suffix + "-" + index, backlogOrderId, backlogOrderNo, talentId, merchantId, huifuId,
                    "backlog-" + suffix + "-" + index);
        }
        assertThat(count("select count(*) from payment_transaction where order_no = ?", backlogOrderNo)).isEqualTo(100);
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        jdbc.update("update payment_transaction set created_at = now() - interval 5 minute where payment_no = ?", paymentNo);
        when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
                "resp_code", "23000001", "resp_desc", "交易不存在", "huifu_id", huifuId)));
        reconciliationService.reconcile(5);
        assertThat(openIssueCount("PAYMENT_PENDING_TIMEOUT", paymentNo))
                .as("刚出现的未决流水不能被历史积压挤出处理窗口——它正是死锁唯一的自愈通道")
                .isEqualTo(1L);
        assertThat(count("select count(*) from payment_transaction where payment_no = ? and last_reconciled_at is not null", paymentNo))
                .isEqualTo(1);
    }
    @Test
@@ -233,8 +472,9 @@
    /** A pending order bound to a collecting sub-merchant, created directly so the payment seam is the only subject. */
    @Test
    void anExpiredOrderWithAnUndecidedPaymentIsNotCancelledByTheDeadlineSweep() {
        String orderNo = expiredOrder();
        String orderNo = pendingOrder();
        String paymentNo = pendingPayment(orderNo, 2000L);
        expireNow(orderNo);
        pendingOrderService.expireDueOrders(talentId);
@@ -338,16 +578,41 @@
    }
    private String pendingPayment(String orderNo, long amount, String status) {
        return pendingPayment(orderNo, amount, status, null);
    }
    private String pendingPayment(String orderNo, long amount, String status, String clientPaymentPayload) {
        String paymentNo = "P-PENDING-" + suffix + "-" + orderSequence;
        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, created_at)
                values (?, 'HUIFU', ?, ?, ?, ?, ?, ?, ?, ?, now())
                """, paymentNo, orderId, orderNo, talentId, merchantId, huifuId, amount, status, "pending-payment-" + paymentNo);
                                                provider_huifu_id, amount, status, idempotency_key, created_at,
                                                client_payment_payload)
                values (?, 'HUIFU', ?, ?, ?, ?, ?, ?, ?, ?, now(), ?)
                """, paymentNo, orderId, orderNo, talentId, merchantId, huifuId, amount, status,
                "pending-payment-" + paymentNo, clientPaymentPayload);
        return paymentNo;
    }
    /** 把这一行推回对账窗口再跑一轮;断言只按本用例自己的流水号过滤,共享库里的残留行不影响结论。 */
    private void reconcileOnce(String paymentNo) {
        jdbc.update("""
                update payment_transaction set created_at = now() - interval 5 minute, last_reconciled_at = null
                where payment_no = ?
                """, paymentNo);
        reconciliationService.reconcile(5);
    }
    /**
     * 让订单现在就算过期。
     *
     * <p>必须建完订单与支付尝试之后再推时间:到期清理任务({@code PendingOrderExpiryScheduler},测试里
     * 也开着)会在「订单已过期、但还没有任何支付流水」的那一瞬间把它当成普通超时订单取消掉。
     */
    private void expireNow(String orderNo) {
        jdbc.update("update benefit_order set expires_at = now() - interval 1 minute where order_no = ?", orderNo);
    }
    private Map<String, Object> providerResponse(Map<String, Object> data) {
        return Map.of("data", data);
    }