yeg
2026-09-12 41cf4fe69e47b79de8208a06815e74895d74777a
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
package com.talentcard.server.business.merchant;
 
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.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
 
import java.util.Map;
 
import com.talentcard.server.business.payment.huifu.HuifuMerchantCapabilityGateway;
import com.talentcard.server.business.payment.huifu.HuifuMerchantCapabilityQueryCommand;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
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.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
 
/**
 * Verifies the merchant payment readiness rules with the provider confirmation switched on and a
 * stubbed Huifu capability query.
 */
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {
        "app.payment.huifu.enabled=true",
        "app.payment.huifu.merchant-capability-required=true"
})
class MerchantPaymentReadinessServiceTests {
 
    @Autowired
    private JdbcTemplate jdbc;
 
    @Autowired
    private MerchantPaymentReadinessService readinessService;
 
    @MockitoBean
    private HuifuMerchantCapabilityGateway capabilityGateway;
 
    private Long merchantId;
    private Long storeId;
    private Long applicationId;
    private Long bankCardId;
    private String suffix;
 
    @BeforeEach
    void prepareMerchant() {
        suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into merchant(merchant_name, status, deleted) values (?, 1, 0)", "Readiness 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, "Readiness store " + suffix);
        storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
        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)
                values (?, ?, ?, 'individual', 'Readiness owner', 'Readiness owner', '13800000000', ?, 'approved', ?)
                """, merchantId, storeId, "MO-RDY-" + suffix, "readiness-onboarding-" + suffix, shortReference("H", suffix));
        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_token_no)
                values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '6666', '**** **** **** 6666', ?, 'approved', ?, ?)
                """, merchantId, storeId, "hash-readiness-" + suffix, "readiness-card-" + suffix, shortReference("T", suffix));
        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);
    }
 
    @Test
    void allowsPaymentOnceAutoSettlementIsOffAndEncashmentIsConfirmed() {
        stubProvider("NORMAL", "0", "1");
 
        MerchantPaymentReadinessService.Readiness readiness = readinessService.readiness(merchantId, storeId);
 
        assertThat(readiness.ready()).isTrue();
        assertThat(readiness.reason()).isEmpty();
        assertThat(jdbc.queryForMap("select auto_settle_status as autoSettle, cash_switch_status as cashSwitch, cash_type as cashType, merchant_stat as merchantStat, capability_verified_at as verifiedAt from merchant_onboarding_application where id=?", applicationId))
                .containsEntry("AUTOSETTLE", "0")
                .containsEntry("CASHSWITCH", "1")
                .containsEntry("CASHTYPE", "DM");
        assertThat(jdbc.queryForObject("select capability_verified_at from merchant_onboarding_application where id=?", Object.class, applicationId)).isNotNull();
    }
 
    @Test
    void blocksPaymentWhileAutoSettlementIsStillOn() {
        stubProvider("NORMAL", "1", "1");
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("自动结算");
    }
 
    @Test
    void blocksPaymentWhenEncashmentCapabilityIsUnavailable() {
        stubProvider("NORMAL", "0", "0");
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("取现");
    }
 
    @Test
    void blocksPaymentWhenTheSubMerchantIsFrozen() {
        stubProvider("FREEZEWITHDRAWAL", "0", "1");
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("FREEZEWITHDRAWAL");
    }
 
    @Test
    void blocksPaymentWhenTheProviderRejectsTheCapabilityQuery() {
        when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
                .thenReturn(HuifuMerchantCapabilityGateway.CapabilityResult.failed("99999999", "商户不存在"));
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("待确认");
    }
 
    @Test
    void blocksPaymentWhenTheProviderQueryTimesOut() {
        when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
                .thenThrow(new HuifuPaymentGatewayException("timeout", true, null));
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class);
    }
 
    @Test
    void blocksPaymentWhenOnboardingIsNotApproved() {
        jdbc.update("update merchant_onboarding_application set status='pending' where id=?", applicationId);
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("进件");
        verify(capabilityGateway, never()).query(any(HuifuMerchantCapabilityQueryCommand.class));
    }
 
    @Test
    void blocksPaymentWhenTheSettlementCardHasNoToken() {
        jdbc.update("update merchant_bank_card set provider_token_no=null where id=?", bankCardId);
 
        assertThatThrownBy(() -> readinessService.requirePayable(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("银行卡");
        verify(capabilityGateway, never()).query(any(HuifuMerchantCapabilityQueryCommand.class));
    }
 
    @Test
    void reusesTheConfirmedSnapshotInsteadOfQueryingTheProviderAgain() {
        stubProvider("NORMAL", "0", "1");
        readinessService.requirePayable(merchantId, storeId);
 
        readinessService.requirePayable(merchantId, storeId);
 
        verify(capabilityGateway, times(1)).query(any(HuifuMerchantCapabilityQueryCommand.class));
    }
 
    @Test
    void refreshesTheSnapshotOnDemandForTheAdminView() {
        stubProvider("NORMAL", "1", "1");
        Map<String, Object> capability = readinessService.capability(applicationId);
 
        assertThat(capability).containsEntry("autoSettleDisabled", false).containsEntry("cashAvailable", true);
 
        stubProvider("NORMAL", "0", "1");
        Map<String, Object> refreshed = readinessService.refresh(applicationId);
 
        assertThat(refreshed).containsEntry("autoSettleDisabled", true).containsEntry("cashAvailable", true);
    }
 
    /** Provider identifiers are limited to 32 characters and huifu ids to 18, so tests keep them short. */
    private String shortReference(String prefix, String suffix) {
        return prefix + suffix.substring(Math.max(0, suffix.length() - 8));
    }
 
    private void stubProvider(String merchantStatus, String settleStatus, String cashSwitch) {
        when(capabilityGateway.query(any(HuifuMerchantCapabilityQueryCommand.class)))
                .thenReturn(new HuifuMerchantCapabilityGateway.CapabilityResult(true, "00000000", "成功", merchantStatus, settleStatus, "D1", cashSwitch, "DM"));
    }
}