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
package com.talentcard.server.business.payment.huifu;
 
import static org.assertj.core.api.Assertions.assertThat;
 
import java.util.Map;
 
import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test;
 
/**
 * 个人(小微)商户进件请求里几条容易漏、且漏了就一定被通道驳回的字段。
 *
 * <p>三条规则都是与业务确认过的口径,不是猜的:
 *
 * <ul>
 *   <li>{@code store_header_pic}(F22)是**必填**:三种小微经营类型都要传,只是图片内容要求不同——
 *       线上商品/服务交易传店铺首页截图、流动经营/便民服务传经营现场照、门店场所传实体门头照。
 *       不能因为"线上没有实体店"就整项不传。</li>
 *   <li>证件「长期有效」({@code *_validity_type=1})时**不填**截止日期,非长期必填。</li>
 *   <li>{@code settle_config} 的 {@code fixed_ratio} / {@code constant_amt} 在 D1 下是条件必填,
 *       平台不收这笔钱,填 0.00;完全不传会被判缺参数。</li>
 * </ul>
 */
class HuifuIndividualOnboardingRequiredFieldsTests {
 
    @Test
    void theStorePhotoIsAlwaysCarriedEvenForOnlineOnlyMicroMerchants() {
        Map<String, Object> request = request(command("0", "20120110", "20320110"));
 
        assertThat(request).containsEntry("store_header_pic", "F22-STORE");
        assertThat(request).containsEntry("settle_card_front_pic", "F13-CARD");
    }
 
    /**
     * 线下经营的经营场所材料是**一组三张**:门头照(F22)、内景照(F24)、收银台(F105)。通道的
     * 「商户材料校验服务」在进件这一步就按 F-code 逐张核对,只发门头照会被整笔驳回——报错原文
     * 「缺失图片:F22-店铺门头照图片;F24-店铺内景图片;F105-店铺收银台」(2026-09-29 实测)。
     */
    @Test
    void theOfflineStoreMaterialGroupCarriesAllThreePhotos() {
        Map<String, Object> request = request(command("0", "20120110", "20320110"));
 
        assertThat(request).containsEntry("store_indoor_pic", "F24-STORE-INDOOR");
        assertThat(request).containsEntry("store_cashier_desk_pic", "F105-STORE-CASHIER");
    }
 
    /** 新增的两张图没交时不下发空字符串:空值会换来「参数格式」类的报错,把「缺材料」这条真原因淹掉。 */
    @Test
    void missingStorePhotosAreOmittedInsteadOfSentAsEmptyStrings() {
        HuifuIndividualOnboardingCommand withoutPhotos = new HuifuIndividualOnboardingCommand(
                "MO-TEST-1", "阳野", "511602199803310019", "0", "20120110", "20320110", "15708261096",
                "四川省广安市广安区兴安中街227号3栋1单元6楼2号", "1347747024@qq.com", "5812",
                "510000", "510100", "510104", "F40-ID", "F41-ID",
                "6217003680002760924", "510000", "511600", "01050000", "F13-CARD", "",
                "四川省广安市广安区兴安中街228号1栋101", "", "");
 
        Map<String, Object> request = request(withoutPhotos);
 
        assertThat(request).doesNotContainKey("store_indoor_pic");
        assertThat(request).doesNotContainKey("store_cashier_desk_pic");
    }
 
    @Test
    void aLongTermCertificateOmitsBothDeadlines() {
        Map<String, Object> request = request(command("1", "20120110", ""));
 
        assertThat(request).containsEntry("legal_cert_validity_type", "1");
        assertThat(request).doesNotContainKey("legal_cert_end_date");
        assertThat(cardInfo(request)).doesNotContainKey("cert_end_date");
    }
 
    @Test
    void aFixedTermCertificateCarriesBothDeadlines() {
        Map<String, Object> request = request(command("0", "20120110", "20320110"));
 
        assertThat(request).containsEntry("legal_cert_end_date", "20320110");
        assertThat(cardInfo(request)).containsEntry("cert_validity_type", "0").containsEntry("cert_end_date", "20320110");
    }
 
    /** 结算周期是 D1,节假日结算手续费两项必须下发;平台不收,填 0.00。 */
    @Test
    void theSettlementConfigCarriesTheD1HolidayFeeFields() {
        Map<String, Object> settle = JSON.parseObject(String.valueOf(request(command("0", "20120110", "20320110")).get("settle_config")), Map.class);
 
        assertThat(settle)
                .containsEntry("settle_cycle", "D1")
                .containsEntry("fixed_ratio", "0.00")
                .containsEntry("constant_amt", "0.00")
                .containsEntry("min_amt", "0.00")
                .containsEntry("remained_amt", "0.00");
    }
 
    /** 企业主体同一份 settle_config(个人进件复用它,见 HuifuIndividualOnboardingGateway)。 */
    @Test
    void theEnterpriseSettlementConfigCarriesTheSameHolidayFeeFields() {
        Map<String, Object> settle = HuifuEnterpriseOnboardingGateway.settleConfig();
 
        assertThat(settle).containsEntry("fixed_ratio", "0.00").containsEntry("constant_amt", "0.00");
    }
 
    private static HuifuPaymentProperties properties() {
        HuifuPaymentProperties properties = new HuifuPaymentProperties();
        properties.setEnabled(true);
        properties.setSystemId("6666000220591054");
        properties.setProductId("KAZX");
        properties.setPrivateKey("x");
        properties.setPlatformPublicKey("y");
        properties.setMerchantWechatFeeRate("0.70");
        properties.setMerchantOnlineBusinessType("H5311L");
        properties.setMerchantAgreementUrl("https://example.com/agreement.htm");
        properties.setOnboardingUpperHuifuId("6666000220591054");
        return properties;
    }
 
    private static Map<String, Object> request(HuifuIndividualOnboardingCommand command) {
        return HuifuIndividualOnboardingGateway.buildRequestForTest(properties(), command);
    }
 
    /**
     * 线下口径的两条硬要求:进件的 {@code scene_type=OFFLINE},且 {@code detail_addr} 取**经营地址**、
     * {@code legal_addr} 取**证件地址**——这两个字段以前共用同一个值,线下报备下会被通道驳回或留下错误信息。
     */
    @Test
    void theOfflineSceneTypeCarriesTheBusinessAddressSeparatelyFromTheCertificateAddress() {
        Map<String, Object> request = request(command("0", "20120110", "20320110"));
 
        assertThat(request).containsEntry("scene_type", "OFFLINE");
        assertThat(request).containsEntry("detail_addr", "四川省广安市广安区兴安中街228号1栋101");
        assertThat(request).containsEntry("legal_addr", "四川省广安市广安区兴安中街227号3栋1单元6楼2号");
    }
 
    @SuppressWarnings("unchecked")
    private static Map<String, Object> cardInfo(Map<String, Object> request) {
        return JSON.parseObject(String.valueOf(request.get("card_info")), Map.class);
    }
 
    private static HuifuIndividualOnboardingCommand command(String validityType, String beginDate, String endDate) {
        return new HuifuIndividualOnboardingCommand(
                "MO-TEST-1", "阳野", "511602199803310019", validityType, beginDate, endDate, "15708261096",
                "四川省广安市广安区兴安中街227号3栋1单元6楼2号", "1347747024@qq.com", "5812",
                "510000", "510100", "510104", "F40-ID", "F41-ID",
                "6217003680002760924", "510000", "511600", "01050000", "F13-CARD", "F22-STORE",
                // 经营详细地址刻意与证件地址(第 8 个参数)用不同字符串:取错成证件地址时断言能发现。
                "四川省广安市广安区兴安中街228号1栋101",
                "F24-STORE-INDOOR", "F105-STORE-CASHIER");
    }
}