yeg
2026-09-16 242917765d2711b723e98764396054acef2a7297
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package com.talentcard.server.business.merchant;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Map;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
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;
 
/**
 * 通道侧配置失败的进件**不再关单重来**:保留原商户号和银行卡,失败的步骤由刷新重试。
 *
 * <p>原先的做法是把进件和银行卡一起标成 rejected,好让商户用同一张卡整单重提;规格已明确改成
 * 「基本进件成功后的局部失败保留原商户号和银行卡」。这类申请现在走分阶段模型,
 * {@code closeRetryableApplication} 对它只刷新进度、不释放任何东西。
 *
 * <p>例外是被通道**明确驳回基本资料**的申请:它交回旧的单状态模型({@code supports()} 对
 * {@code rejected} 返回 false),商户端才拿得到「重新提交」入口。
 */
@SpringBootTest
@ActiveProfiles("test")
class MerchantOnboardingRetryReleaseTests {
 
    @Autowired
    private JdbcTemplate jdbc;
 
    @Autowired
    private MerchantSettlementService settlementService;
 
    // 分阶段刷新会先推进开通步骤,这几个网关都要挡住,否则测试会真的打到汇付。
    @MockitoBean private HuifuOnboardingProgressGateway progressGateway;
    @MockitoBean private com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway wechatConfigGateway;
    @MockitoBean private com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway businessOpenGateway;
 
    private Long merchantId;
    private Long storeId;
    private Long applicationId;
    private Long bankCardId;
    private String suffix;
 
    @BeforeEach
    void prepare() {
        suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into merchant(merchant_name, status, deleted) values (?, 1, 0)", "Retry merchant " + 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, "Retry store " + suffix);
        storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
    }
 
    @Test
    void aFailedProvisioningKeepsTheApplicationAndItsBankCard() {
        seedApplication("failed", "not_started");
        stubProvider();
 
        settlementService.closeRetryableApplication(principal());
 
        // 关键变化:不再释放。进件与银行卡都留着,失败的开通步骤由刷新去重试——
        // 释放掉就等于让商户整单重来,与「保留原商户号和银行卡」相悖。
        assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
        assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
    }
 
    @Test
    void aFailedBusinessOpenKeepsTheCardForRetry() {
        seedApplication("approved", "failed");
        stubProvider();
 
        settlementService.closeRetryableApplication(principal());
 
        assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
        assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
    }
 
    @Test
    void provisioningStillInFlightIsLeftAlone() {
        seedApplication("processing", "not_started");
 
        settlementService.closeRetryableApplication(principal());
 
        assertThat(status("merchant_onboarding_application", applicationId)).isEqualTo("pending");
        assertThat(status("merchant_bank_card", bankCardId)).isEqualTo("pending");
    }
 
    /** 被驳回的申请回到旧的单状态模型,刷新不再接管它——否则会被翻回 pending,重提入口消失。 */
    @Test
    void aRejectedApplicationIsLeftToTheSingleStatusModel() {
        seedApplication("failed", "failed");
        jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason='基本资料审核未通过' where id=?", applicationId);
 
        Map<String,Object> view = settlementService.refreshProgress(applicationId, false);
 
        assertThat(view).containsEntry("status", "rejected");
        verifyNoInteractions(progressGateway);
    }
 
    private void stubProvider() {
        when(progressGateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(progressGateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
        when(progressGateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000"));
    }
 
    private void seedApplication(String wechatConfigStatus, String businessOpenStatus) {
        String applicationNo = "MO-RT-" + suffix + "-" + wechatConfigStatus;
        jdbc.update("""
                insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name,
                                                            contact_name, contact_mobile, idempotency_key, status,
                                                            wechat_config_status, business_open_status)
                values (?, ?, ?, 'individual', 'Retry owner', 'Retry owner', '13700000001', ?, 'pending', ?, ?)
                """, merchantId, storeId, applicationNo, "retry-onboarding-" + suffix, wechatConfigStatus, businessOpenStatus);
        applicationId = jdbc.queryForObject("select max(id) from merchant_onboarding_application", Long.class);
        jdbc.update("""
                insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, city_code, card_tail, card_masked,
                                               card_hash, status, idempotency_key, provider_request_no)
                values (?, ?, '中国建设银行', '广安人民路支行', '511600', '0924', '**** **** **** 0924', ?, 'pending', ?, ?)
                """, merchantId, storeId, "hash-retry-" + suffix, "retry-card-" + suffix, applicationNo);
        bankCardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", bankCardId, applicationId);
    }
 
    private MerchantPrincipal principal() { return new MerchantPrincipal(0L, merchantId, storeId, "operator"); }
 
    private String status(String table, Long id) {
        return jdbc.queryForObject("select status from " + table + " where id = ?", String.class, id);
    }
}