yeg
2026-09-16 815d8a436a30d572ebf09817402bf35beb929aea
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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.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;
 
@SpringBootTest
@ActiveProfiles("test")
class MerchantExistingOnboardingTests {
    @Autowired JdbcTemplate jdbc;
    @Autowired MerchantSettlementService service;
    @MockitoBean HuifuOnboardingProgressGateway gateway;
    // 刷新现在会先推进开通步骤(微信配置、业务开通),所以这两个网关也要挡住,
    // 否则测试会真的打到汇付。
    @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway wechatConfigGateway;
    @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway businessOpenGateway;
    @Autowired org.springframework.web.context.WebApplicationContext context;
    @Autowired com.talentcard.server.business.admin.auth.AdminAuthService adminAuth;
 
    @Test
    void successfulDelayCallbackIgnoresLateFailureForTheSameAttempt() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","N","forced_delay_flag","N"));
        when(gateway.configureDelay(anyString(),anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_no","delay-apply"));
        service.refreshOnboarding(principal,false);
        var number=org.mockito.ArgumentCaptor.forClass(String.class);
        var date=org.mockito.ArgumentCaptor.forClass(String.class);
        verify(gateway).configureDelay(anyString(),number.capture(),date.capture());
        assertThat(service.handleBusinessOpenCallback(Map.of("huifu_id","6666000241335656","req_seq_id",number.getValue(),"req_date",date.getValue(),"audit_status","Y","sub_resp_code","00000000"))).isTrue();
        assertThat(service.handleBusinessOpenCallback(Map.of("huifu_id","6666000241335656","req_seq_id",number.getValue(),"req_date",date.getValue(),"audit_status","N","sub_resp_code","99999999"))).isTrue();
        assertThat(service.refreshOnboarding(principal,true)).containsEntry("retryable",false);
        verify(gateway,times(1)).configureDelay(anyString(),anyString(),anyString());
    }
 
    @Test
    void completesOnlyAfterEveryCapabilityAndTheOriginalCardAreConfirmed() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        jdbc.update("update merchant_bank_card set card_hash=? where merchant_id=?",sha256("622200001234"),principal.merchantId());
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S","bind_card_status","S"));
        var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2",
                "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","wx-merchant","pay_channel_id","00016904","is_default","1")))).toList();
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes,
                "qry_settle_config_list",java.util.List.of(Map.of("settle_status","1","settle_cycle","D1","settle_pattern","P0","min_amt","0","remained_amt","0","token_no","original-card-token")),
                "qry_cash_card_info_list",java.util.List.of(Map.of("card_no","622200001234","token_no","original-card-token"))));
        when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","01","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179"))))));
        when(gateway.queryRealname(anyString())).thenReturn(Map.of("resp_code","00000000","applyment_stat","APPLYMENT_STATE_PASSED","authorize_stat","1"));
        assertThat(service.refreshOnboarding(principal,false)).containsEntry("status","approved");
        assertThat(service.onboardingStatus(principal)).containsEntry("status","approved");
        assertThat(service.bankCards(principal)).hasSize(1);
        verify(gateway,never()).configureDelay(anyString(),anyString(),anyString());
    }
 
    private static String sha256(String value) {
        try { return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))); }
        catch (Exception exception) { throw new IllegalStateException(exception); }
    }
 
    @Test
    void concurrentRefreshCannotCreateASecondDelayAttempt() throws Exception {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","N","forced_delay_flag","N"));
        var entered=new java.util.concurrent.CountDownLatch(1);
        var release=new java.util.concurrent.CountDownLatch(1);
        when(gateway.configureDelay(anyString(),anyString(),anyString())).thenAnswer(call -> {
            entered.countDown();
            if (!release.await(10,java.util.concurrent.TimeUnit.SECONDS)) throw new IllegalStateException("Test release timed out");
            return Map.of("resp_code","00000000","apply_no","concurrent-apply");
        });
        var executor=java.util.concurrent.Executors.newSingleThreadExecutor();
        try {
            var first=executor.submit(() -> service.refreshOnboarding(principal,false));
            assertThat(entered.await(10,java.util.concurrent.TimeUnit.SECONDS)).isTrue();
            service.refreshOnboarding(principal,true);
            release.countDown();
            first.get(10,java.util.concurrent.TimeUnit.SECONDS);
            verify(gateway,times(1)).configureDelay(anyString(),anyString(),anyString());
        } finally { release.countDown(); executor.shutdownNow(); }
    }
 
    @Test
    void retriesOnlyAnExplicitlyFailedDelayAttemptWithANewRequestNumber() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","N","forced_delay_flag","N"));
        when(gateway.configureDelay(anyString(),anyString(),anyString())).thenReturn(Map.of("resp_code","20002"),Map.of("resp_code","00000000","apply_no","retry-apply"));
        assertThat(service.refreshOnboarding(principal,false)).containsEntry("retryable",true);
        service.refreshOnboarding(principal,false);
        service.refreshOnboarding(principal,true);
        var requests=org.mockito.ArgumentCaptor.forClass(String.class);
        verify(gateway,times(2)).configureDelay(eq("6666000241335656"),requests.capture(),anyString());
        assertThat(requests.getAllValues()).doesNotHaveDuplicates();
    }
 
    @Test
    void oldBasicFailureCallbackCannotRejectAnExistingVerifiedMerchantOrCard() {
        Map<String,Object> fixture=fixture();
        MerchantPrincipal principal=(MerchantPrincipal)fixture.get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
        service.refreshOnboarding(principal,false);
        service.handleOnboardingCallback(Map.of("req_seq_id",fixture.get("applicationNo"),"huifu_id","6666000241335656","audit_status","N","sub_resp_code","99999999"));
        assertThat(service.bankCards(principal)).allSatisfy(card -> assertThat(card.get("status")).isEqualTo("approved"));
        assertThat(service.onboardingStatus(principal)).containsEntry("status","pending");
    }
 
    @Test
    void unknownDelayUpdateIsQueriedBeforeRetryAndNeverSubmittedTwice() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","N","forced_delay_flag","N"));
        when(gateway.configureDelay(anyString(),anyString(),anyString())).thenThrow(new com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException("timeout",true,null));
        service.refreshOnboarding(principal,false);
        Map<String,Object> result=service.refreshOnboarding(principal,true);
        verify(gateway,times(1)).configureDelay(eq("6666000241335656"),matches("[a-f0-9]{32}"),matches("[0-9]{8}"));
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","delay").containsEntry("status","pending"));
        assertThat(result).containsEntry("retryable",false).containsEntry("status","pending");
    }
 
    @Test
    void failedRealnameQueryPreservesConfirmedStateAndReportsQueryFailure() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
        when(gateway.queryRealname(anyString())).thenReturn(Map.of("resp_code","00000000","applyment_stat","APPLYMENT_STATE_PASSED","authorize_stat","1"),Map.of("resp_code","20002"));
        service.refreshOnboarding(principal,false);
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","realname").containsEntry("status","confirmed"));
        assertThat(result.get("syncError")).isEqualTo("汇付查询权限不足(20002),请联系平台运营核实接口权限");
    }
 
    @Test
    void realnamePassedStillNeedsAuthorizationAndControlledAppIdIsNotReady() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
        when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",java.util.List.of(Map.of("fee_type","01","wx_applet_app_id","wxcfee477acb54d179","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179","control_flag","Y"))))));
        when(gateway.queryRealname(anyString())).thenReturn(Map.of("resp_code","00000000","applyment_stat","APPLYMENT_STATE_PASSED","authorize_stat","0"));
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","appId").containsEntry("status","action_required"));
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","realname").containsEntry("status","action_required"));
        assertThat(result).containsEntry("status","pending");
    }
 
    @Test
    void ordinaryReadsExposeSameProgressWithoutCallingHuifu() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","N","forced_delay_flag","N"));
        Map<String,Object> refreshed=service.refreshOnboarding(principal,false);
        clearInvocations(gateway);
        assertThat(service.onboardingStatus(principal).get("stages")).isEqualTo(refreshed.get("stages"));
        verifyNoInteractions(gateway);
    }
 
    @Test
    void refreshRequiresSettlementEditPermissionEvenForAnAuthenticatedAdmin() throws Exception {
        String account="t02admin"+System.nanoTime();
        jdbc.update("insert into admin_operator(account,password_hash,display_name,role_code,status,deleted) values (?,?,?,'t02-no-rights',1,0)",account,
                com.talentcard.server.business.merchant.auth.MerchantPasswordHasher.encode("TestPass123"),account);
        String token=String.valueOf(adminAuth.login(new com.talentcard.server.business.admin.auth.AdminLoginRequest(account,"TestPass123"),account).get("accessToken"));
        org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup(context).build().perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/admin/management/merchant-settlement/onboardings/449/progress/refresh")
                .header("Authorization","Bearer "+token)).andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isForbidden());
        verifyNoInteractions(gateway);
    }
 
    @Test
    void configuredWechatScenesStillRequireTalentAppAndAuthorization() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        var scenes=java.util.List.of("4","5","12").stream().map(scene -> Map.of("pay_scene",scene,"fee_rate","0.70","fee_min_amt","0.00","fee_charge_type","1","out_fee_flag","2",
                "wx_mer_infos",java.util.List.of(Map.of("bank_mer_code","wx-merchant","pay_channel_id","00016904","is_default","1")))).toList();
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N","qry_wx_conf_list",scenes));
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","wechat").containsEntry("status","confirmed"));
        assertThat(result).containsEntry("status","pending");
    }
 
    @Test
    void confirmsD1OnlyFromEffectiveZeroThresholdBatchSettlement() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL","delay_flag","Y","forced_delay_flag","N",
                "qry_settle_config_list","[{\"settle_status\":\"1\",\"settle_cycle\":\"D1\",\"settle_pattern\":\"P0\",\"min_amt\":0,\"remained_amt\":0}]"));
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","settlement").containsEntry("status","confirmed"));
        assertThat(result).containsEntry("status","pending");
    }
 
    @Test
    void submittingAgainCannotReplaceDesignatedExistingMerchantAfterConfigFailure() {
        Map<String,Object> application=fixture();
        jdbc.update("update merchant_onboarding_application set wechat_config_status='failed' where application_no=?",application.get("applicationNo"));
        Map<String,Object> result=service.submitOnboarding((MerchantPrincipal)application.get("principal"),null,"T02-retry-"+System.nanoTime());
        assertThat(result.get("applicationNo")).isEqualTo(application.get("applicationNo"));
    }
 
    @Test
    void missingDelayFieldsKeepLastConfirmedEvidenceAndReportIncompleteQuery() {
        MerchantPrincipal principal=(MerchantPrincipal)fixture().get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"),Map.of("resp_code","00000000"));
        service.refreshOnboarding(principal,false);
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","delay").containsEntry("status","confirmed"));
        assertThat(result.get("syncError")).isEqualTo("汇付返回缺少延迟配置字段,已保留最后有效状态");
    }
 
    @Test
    void capabilityQueryErrorKeepsConfirmedDelayAndReportsSyncError() {
        Map<String,Object> application=fixture();
        MerchantPrincipal principal=(MerchantPrincipal)application.get("principal");
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"),Map.of("resp_code","20002","resp_desc","数据权限认证失败"));
        service.refreshOnboarding(principal,false);
        Map<String,Object> result=service.refreshOnboarding(principal,false);
        assertThat(result.get("syncError")).isEqualTo("汇付查询权限不足(20002),请联系平台运营核实接口权限");
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","delay").containsEntry("status","confirmed"));
    }
 
    @Test
    void approvedBasicApplicationIsNotCompleteWithoutWechatAndFundsEvidence() {
        Map<String,Object> application=fixture();
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S","bind_card_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","mer_stat","NORMAL"));
        Map<String,Object> result=service.refreshOnboarding((MerchantPrincipal)application.get("principal"),false);
        assertThat(result).containsEntry("status","pending");
        assertThat((java.util.List<Map<String,Object>>)result.get("stages")).anySatisfy(stage -> assertThat(stage).containsEntry("key","basic").containsEntry("status","confirmed"));
        assertThat(result.get("lastSyncedAt")).isNotEqualTo("");
    }
 
    private Map<String,Object> fixture() {
        String key="T02-"+System.nanoTime();
        jdbc.update("insert into merchant(merchant_name,status,deleted) values (?,1,0)",key);
        Long merchant=jdbc.queryForObject("select id from merchant where merchant_name=?",Long.class,key);
        jdbc.update("insert into merchant_store(merchant_id,store_name,status,deleted) values (?,?,1,0)",merchant,key);
        Long store=jdbc.queryForObject("select id from merchant_store where merchant_id=?",Long.class,merchant);
        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) values (?,?,'Bank','Branch','510100','1234','****1234',?,'approved',?)",merchant,store,key,key);
        Long card=jdbc.queryForObject("select id from merchant_bank_card where idempotency_key=?",Long.class,key);
        jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id,wechat_config_status,business_open_status) values (?, ?, ?, 'enterprise','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?,'approved','approved')",merchant,store,key,key,card);
        return Map.of("principal",new MerchantPrincipal(1L,merchant,store,"owner"),"applicationNo",key);
    }
 
    /**
     * 分阶段刷新要**先推进开通步骤,再回报结果**。渠道侧的微信小程序支付配置与商户业务开通原本
     * 只由进件审核回调触发,而分阶段申请的回调走的是进度刷新——不在这里补上推进,新商户的微信
     * 开通就永远不会被发起。
     */
    @Test
    void refreshingAdvancesTheWechatProvisioningSteps() {
        Map<String,Object> fixture=provisioningFixture();
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N"));
        when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000"));
        when(wechatConfigGateway.configure(anyString(),anyString())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway.Result(true,"00000000","成功"));
        when(businessOpenGateway.open(anyString(),anyString(),anyString(),anyString(),anyBoolean())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway.Result(true,"BO-APPLY","90000000","审核中"));
 
        service.refreshOnboarding((MerchantPrincipal)fixture.get("principal"),false);
 
        assertThat(statusOf("wechat_config_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("approved");
        assertThat(statusOf("business_open_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("processing");
        verify(wechatConfigGateway,times(1)).configure(anyString(),eq("6666000241335656"));
        verify(businessOpenGateway,times(1)).open(anyString(),eq("6666000241335656"),anyString(),anyString(),anyBoolean());
    }
 
    /**
     * 通道侧已经生效的步骤不再重复提交。微信商户配置是**添加**接口(成功后不支持改删、每种参数
     * 最多 5 组),业务开通是申请型接口,重复提交会在通道留下重复配置或第二份申请。
     */
    @Test
    void refreshingSkipsStepsAlreadyEffectiveOnTheChannel() {
        Map<String,Object> fixture=provisioningFixture();
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","00000000","apply_status","Y","reg_status","S"));
        when(gateway.queryCapabilities(anyString())).thenReturn(Map.of("resp_code","00000000","delay_flag","Y","forced_delay_flag","N",
                "qry_wx_conf_list",java.util.List.of(Map.of("pay_scene","4","fee_rate","0.70"))));
        when(gateway.queryWechatConfig(anyString())).thenReturn(Map.of("resp_code","00000000","atu_config_bo_list",
                java.util.List.of(Map.of("fee_type","01","wx_config_list",java.util.List.of(Map.of("config_type","1","config_value","wxcfee477acb54d179"))))));
 
        service.refreshOnboarding((MerchantPrincipal)fixture.get("principal"),false);
 
        assertThat(statusOf("wechat_config_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("approved");
        assertThat(statusOf("business_open_status",String.valueOf(fixture.get("applicationNo")))).isEqualTo("approved");
        verify(wechatConfigGateway,never()).configure(anyString(),anyString());
        verify(businessOpenGateway,never()).open(anyString(),anyString(),anyString(),anyString(),anyBoolean());
    }
 
    private String statusOf(String column,String applicationNo) {
        return jdbc.queryForObject("select "+column+" from merchant_onboarding_application where application_no=?",String.class,applicationNo);
    }
 
    /** 建一张还停在 not_started 的进件,用来观察刷新会不会去推进开通步骤。 */
    private Map<String,Object> provisioningFixture() {
        String key="T02P-"+System.nanoTime();
        jdbc.update("insert into merchant(merchant_name,status,deleted) values (?,1,0)",key);
        Long merchant=jdbc.queryForObject("select id from merchant where merchant_name=?",Long.class,key);
        jdbc.update("insert into merchant_store(merchant_id,store_name,status,deleted) values (?,?,1,0)",merchant,key);
        Long store=jdbc.queryForObject("select id from merchant_store where merchant_id=?",Long.class,merchant);
        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) values (?,?,'Bank','Branch','510100','1234','****1234',?,'approved',?)",merchant,store,key,key);
        Long card=jdbc.queryForObject("select id from merchant_bank_card where idempotency_key=?",Long.class,key);
        jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id) values (?, ?, ?, 'enterprise','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?)",merchant,store,key,key,card);
        return Map.of("principal",new MerchantPrincipal(1L,merchant,store,"owner"),"applicationNo",key);
    }
 
    @Test
    void queryPermissionErrorPreservesOriginalApplicationAndCard() {
        String key = "T02-" + System.nanoTime();
        jdbc.update("insert into merchant(merchant_name,status,deleted) values (?,1,0)", key);
        Long merchant = jdbc.queryForObject("select id from merchant where merchant_name=?", Long.class,key);
        jdbc.update("insert into merchant_store(merchant_id,store_name,status,deleted) values (?,?,1,0)",merchant,key);
        Long store = jdbc.queryForObject("select id from merchant_store where merchant_id=?",Long.class,merchant);
        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) values (?,?,'Bank','Branch','510100','1234','****1234',?,'approved',?)",merchant,store,key,key);
        Long card = jdbc.queryForObject("select id from merchant_bank_card where idempotency_key=?",Long.class,key);
        jdbc.update("insert into merchant_onboarding_application(merchant_id,store_id,application_no,subject_type,subject_name,contact_name,contact_mobile,idempotency_key,status,provider_huifu_id,provider_apply_no,initial_bank_card_id,wechat_config_status,business_open_status) values (?, ?, ?, 'enterprise','Owner','Owner','13700000000',?,'pending','6666000241335656','original-apply',?,'approved','approved')",merchant,store,key,key,card);
        when(gateway.queryApplication(anyString(),anyString())).thenReturn(Map.of("resp_code","20002","resp_desc","数据权限认证失败"));
        Map<String,Object> result=service.refreshOnboarding(new MerchantPrincipal(1L,merchant,store,"owner"),false);
        assertThat(result).containsEntry("applicationNo",key).containsEntry("status","pending").containsEntry("syncError","汇付查询权限不足(20002),请联系平台运营核实接口权限");
        assertThat(service.bankCards(new MerchantPrincipal(1L,merchant,store,"owner"))).isNotNull();
        assertThat(jdbc.queryForObject("select initial_bank_card_id from merchant_onboarding_application where application_no=?",Long.class,key)).isEqualTo(card);
    }
}