yeg
2026-09-18 70ea0911ce7519ca802266078624e9fed3559a2c
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
package com.talentcard.server.business.portal;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
import java.util.Map;
 
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;
 
@SpringBootTest
@ActiveProfiles("test")
class AppOrderPortalServiceTests {
    @Autowired
    private JdbcTemplate jdbc;
 
    @Autowired
    private AppOrderPortalService appOrderPortalService;
 
    private Long talentId;
    private Long merchantId;
    private Long productId;
    private Long couponId;
    private Long onboardingId;
 
    @BeforeEach
    void prepareData() {
        String suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)", "app-order-" + suffix, "App order user", "13900000000");
        talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
        jdbc.update("insert into talent_card(talent_id, card_no, card_type, card_level, status, deleted) values (?, ?, 'PERSONAL', ?, 1, 0)", talentId, "TC-APP-" + suffix, "卓越人才卡");
        jdbc.update("insert into merchant(merchant_name, contact_mobile, address, status, deleted) values (?, ?, ?, 1, 0)", "App order merchant " + suffix, "08120000000", "Test address");
        merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
        // A merchant may only take paid orders once onboarding is approved and settlement capabilities are confirmed.
        onboardingId = readyMerchant(merchantId, "app-order-" + suffix);
        jdbc.update("""
                insert into merchant_product(product_no, merchant_id, product_type, product_name, original_price, sale_price,
                                             subsidy_amount, stock, audit_status, status, deleted)
                values (?, ?, 'food', 'Test package', 100.00, 60.00, 0.00, 10, 2, 1, 0)
                """, "APP-ORDER-PRODUCT-" + suffix, merchantId);
        productId = jdbc.queryForObject("select max(id) from merchant_product", Long.class);
        jdbc.update("""
                insert into merchant_coupon(merchant_id, coupon_name, condition_type, coupon_amount, product_scope, status, deleted)
                values (?, 'Test coupon', 'NONE', 10.00, 'ALL', 1, 0)
                """, merchantId);
        couponId = jdbc.queryForObject("select max(id) from merchant_coupon", Long.class);
    }
 
    /** Creates a store, an approved onboarding and an approved settlement card for the merchant. */
    private Long readyMerchant(Long merchant, String suffix) {
        jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", merchant, "App order store " + suffix);
        Long 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,
                                                            auto_settle_status, cash_switch_status, merchant_stat, capability_verified_at)
                values (?, ?, ?, 'individual', 'App order owner', 'App order owner', '13900000000', ?, 'approved', ?, '0', '1', 'NORMAL', current_timestamp)
                """, merchant, storeId, "MO-APP-" + suffix, "app-order-onboarding-" + suffix, shortReference("H", suffix));
        Long 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, reviewed_at)
                values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '8888', '**** **** **** 8888', ?, 'approved', ?, ?, current_timestamp)
                """, merchant, storeId, "hash-app-order-" + suffix, "app-order-card-" + suffix, shortReference("T", suffix));
        Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, applicationId);
        // 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步,
        // 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是下单的测试。
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId);
        return applicationId;
    }
 
    /** Provider identifiers are limited to 32 characters, so tests keep them short. */
    private String shortReference(String prefix, String suffix) {
        return prefix + suffix.substring(Math.max(0, suffix.length() - 8));
    }
 
    @Test
    void createsPendingOrderFromServerSideProductAndCouponSnapshot() {
        Map<String, Object> created = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, couponId), "app-order-test-" + System.nanoTime());
 
        assertThat(created.get("orderNo")).asString().startsWith("P");
        assertThat(created).containsEntry("talentId", talentId).containsEntry("merchantId", merchantId).containsEntry("productId", productId);
        Map<String, Object> row = jdbc.queryForMap("""
                select order_amount as orderAmount, base_pay_amount as basePayAmount, coupon_amount as couponAmount,
                       pay_amount as payAmount, order_status as orderStatus, order_type as orderType,
                       product_name_snapshot as productNameSnapshot
                from benefit_order
                where order_no = ?
                """, created.get("orderNo"));
        assertThat(row).containsEntry("ORDERAMOUNT", 10000L)
                .containsEntry("BASEPAYAMOUNT", 6000L)
                .containsEntry("COUPONAMOUNT", 1000L)
                .containsEntry("PAYAMOUNT", 5000L)
                .containsEntry("ORDERSTATUS", 0)
                .containsEntry("ORDERTYPE", "GROUP_BUY");
        assertThat(row.get("PRODUCTNAMESNAPSHOT")).isEqualTo("Test package");
    }
 
    @Test
    void reusesOrderForSameIdempotencyKey() {
        String idempotencyKey = "app-order-repeat-" + System.nanoTime();
        Map<String, Object> first = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey);
        Map<String, Object> second = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey);
 
        assertThat(second.get("orderNo")).isEqualTo(first.get("orderNo"));
    }
 
    @Test
    void cancelsExpiredPendingOrderBeforeReusingItsCoupon() {
        Map<String, Object> expired = appOrderPortalService.create(
                talentId,
                new AppOrderCreateRequest(merchantId, productId, couponId),
                "app-order-expired-first-" + System.nanoTime());
        jdbc.update("update benefit_order set expires_at = current_timestamp - interval '1' minute where order_no = ?", expired.get("orderNo"));
 
        Map<String, Object> replacement = appOrderPortalService.create(
                talentId,
                new AppOrderCreateRequest(merchantId, productId, couponId),
                "app-order-expired-replacement-" + System.nanoTime());
 
        assertThat(replacement.get("orderNo")).isNotEqualTo(expired.get("orderNo"));
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, expired.get("orderNo"))).isEqualTo(2);
        assertThat(jdbc.queryForObject("select coupon_id from benefit_order where order_no = ?", Long.class, replacement.get("orderNo"))).isEqualTo(couponId);
    }
 
    /** 自动结算开着不再阻止下单:未核销的钱由延迟户兜住,不会被自动结算提前结走。 */
    @Test
    void allowsPaidOrderEvenWhenMerchantAutoSettlementIsOn() {
        jdbc.update("update merchant_onboarding_application set auto_settle_status='1' where id=?", onboardingId);
 
        Map<String, Object> created = appOrderPortalService.create(talentId,
                new AppOrderCreateRequest(merchantId, productId, null), "app-order-auto-settle-" + System.nanoTime());
        assertThat(created.get("orderNo")).isNotNull();
    }
 
    @Test
    void refusesPaidOrderWhenMerchantOnboardingIsNotApproved() {
        jdbc.update("update merchant_onboarding_application set status='pending', capability_verified_at=null where id=?", onboardingId);
        jdbc.update("update merchant_bank_card set status='pending', provider_token_no=null where merchant_id=?", merchantId);
 
        assertThatThrownBy(() -> appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-pending-" + System.nanoTime()))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("进件");
    }
 
    @Test
    void fixesTheCollectingSubMerchantOnThePaidOrder() {
        Map<String, Object> created = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-collector-" + System.nanoTime());
 
        Map<String, Object> onboarding = jdbc.queryForMap("select store_id, provider_huifu_id from merchant_onboarding_application where id = ?", onboardingId);
        Map<String, Object> order = jdbc.queryForMap("""
                select collect_onboarding_id, collect_store_id, collect_huifu_id
                from benefit_order where order_no = ?
                """, created.get("orderNo"));
        assertThat(order).containsEntry("collect_onboarding_id", onboardingId)
                .containsEntry("collect_store_id", onboarding.get("store_id"))
                .containsEntry("collect_huifu_id", onboarding.get("provider_huifu_id"));
    }
 
    @Test
    void leavesTheCollectingSubMerchantEmptyOnAFreeOrder() {
        jdbc.update("""
                update merchant_product
                set rights_config = ?
                where id = ?
                """, "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"exceedDiscount\":6,\"resetCycle\":\"每月\"}]", productId);
 
        Map<String, Object> freeOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-free-collector-" + System.nanoTime());
 
        Map<String, Object> order = jdbc.queryForMap("""
                select pay_amount, collect_onboarding_id, collect_huifu_id
                from benefit_order where order_no = ?
                """, freeOrder.get("orderNo"));
        assertThat(order).containsEntry("pay_amount", 0L)
                .containsEntry("collect_onboarding_id", null)
                .containsEntry("collect_huifu_id", null);
    }
 
    @Test
    void refusesPaidOrderWhenTheApprovedOnboardingHasNoHuifuId() {
        jdbc.update("update merchant_onboarding_application set provider_huifu_id=null where id=?", onboardingId);
 
        assertThatThrownBy(() -> appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-no-huifu-" + System.nanoTime()))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("斗拱商户号");
    }
 
    @Test
    void usesAvailableFreeCountBeforeChargingTheProductPrice() {
        jdbc.update("""
                update merchant_product
                set rights_config = ?
                where id = ?
                """, "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"exceedDiscount\":6,\"resetCycle\":\"每月\"}]", productId);
 
        Map<String, Object> freeOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-free-" + System.nanoTime());
        Long freePayAmount = jdbc.queryForObject("select pay_amount from benefit_order where order_no = ?", Long.class, freeOrder.get("orderNo"));
        assertThat(freePayAmount).isZero();
 
        Map<String, Object> paidOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-after-free-" + System.nanoTime());
        Long paidAmount = jdbc.queryForObject("select pay_amount from benefit_order where order_no = ?", Long.class, paidOrder.get("orderNo"));
        assertThat(paidAmount).isEqualTo(6000L);
    }
}