shinnosuke
2026-08-28 3eb95d53e1037cc5e10b1da3a62b0ff4d5fc9536
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
package com.talentcard.server.business.merchant;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import com.talentcard.server.business.portal.FreeBenefitPortalService;
import com.talentcard.server.business.portal.PendingOrderPaymentRequest;
import com.talentcard.server.business.portal.PendingOrderPortalService;
import com.talentcard.server.business.portal.PendingUseCodeRequest;
import com.talentcard.server.business.portal.PendingUsePortalService;
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 MerchantVerificationServiceTests {
 
    @Autowired
    private JdbcTemplate jdbc;
 
    @Autowired
    private MerchantVerificationService verificationService;
 
    @Autowired
    private ManualConsumptionService manualConsumptionService;
 
    @Autowired
    private PendingOrderPortalService pendingOrderPortalService;
 
    @Autowired
    private PendingUsePortalService pendingUsePortalService;
 
    @Autowired
    private DashboardSummaryService dashboardSummaryService;
 
    @Autowired
    private FreeBenefitPortalService freeBenefitPortalService;
 
    private Long merchantId;
    private Long storeId;
    private Long talentId;
    private Long cardId;
    private MerchantPrincipal principal;
    private String token;
 
    @BeforeEach
    void prepareVerification() {
        jdbc.update("insert into merchant(merchant_name, status, can_login_merchant_app, deleted) values (?, 1, 1, 0)", "Verification test merchant");
        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, "Verification test store");
        storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
        jdbc.update("""
                insert into merchant_store_consume_rule(rule_no, merchant_id, card_level, rule_name, discount_rate, subsidy_enabled, status)
                values (?, ?, 'EXCELLENT', 'Store consume 10 percent off', 0.9000, 1, 1)
                """, "rule-" + System.nanoTime(), merchantId);
        String suffix = String.valueOf(System.nanoTime());
        jdbc.update("""
                insert into talent_user(openid, name, mobile, talent_level, status, deleted)
                values (?, ?, ?, ?, 1, 0)
                """, "verification-test-" + suffix, "核销测试用户", "13800000000", "卓越人才卡");
        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, "VERIFICATION-" + suffix);
        cardId = jdbc.queryForObject("select max(id) from talent_card", Long.class);
        token = "merchant-verification-test-" + System.nanoTime();
        jdbc.update("""
                insert into talent_dynamic_code(code_token, card_id, talent_id, expected_merchant_id, scene, expires_at, status)
                values (?, ?, ?, ?, 'merchant', ?, 0)
                """, token, cardId, talentId, merchantId, Timestamp.valueOf(LocalDateTime.now().plusMinutes(1)));
        principal = new MerchantPrincipal(999L, merchantId, storeId, "operator");
    }
 
    @Test
    void talentCodeCreatesPendingPaymentOrderInsteadOfDirectVerification() {
        Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(token));
        Integer beforeCreate = jdbc.queryForObject("select status from talent_dynamic_code where code_token = ?", Integer.class, token);
        assertThat(String.valueOf(scanned.get("verificationId"))).isNotBlank();
        assertThat(scanned.get("credentialType")).isEqualTo("TALENT_CODE");
        assertThat(beforeCreate).isZero();
 
        Map<String, Object> detail = manualConsumptionService.detail(principal, String.valueOf(scanned.get("verificationId")));
        String ruleId = String.valueOf(((java.util.List<?>) detail.get("rules")).get(0) instanceof Map<?, ?> rule ? rule.get("id") : "");
        ManualConsumptionCreateRequest request = new ManualConsumptionCreateRequest(String.valueOf(scanned.get("verificationId")), Long.valueOf(ruleId), new BigDecimal("680.00"));
        Map<String, Object> quoted = manualConsumptionService.quote(principal, request);
        Map<String, Object> created = manualConsumptionService.create(principal, request, "manual-consumption-key");
        Map<String, Object> repeated = manualConsumptionService.create(principal, request, "manual-consumption-key");
        Integer afterCreate = jdbc.queryForObject("select status from talent_dynamic_code where code_token = ?", Integer.class, token);
 
        assertThat(quoted).containsEntry("originalAmount", 68000L).containsEntry("payAmount", 61200L).containsEntry("subsidyAmount", 6800L);
        assertThat(created.get("orderNo")).isEqualTo(repeated.get("orderNo"));
        assertThat(afterCreate).isEqualTo(1);
        assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, created.get("orderNo"))).isZero();
        assertThat(jdbc.queryForObject("select count(*) from verify_record where talent_id = ?", Integer.class, talentId)).isZero();
        assertThatThrownBy(() -> verificationService.confirm(principal,
                new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "verification-test-key"))
                .isInstanceOf(com.talentcard.server.common.BusinessException.class);
 
        pendingOrderPortalService.pay(String.valueOf(created.get("orderNo")),
                new PendingOrderPaymentRequest(talentId, ((Number) created.get("priceVersion")).intValue()));
        assertThat(jdbc.queryForObject("select verify_status from benefit_order where order_no = ?", Integer.class, created.get("orderNo"))).isEqualTo(1);
        assertThatThrownBy(() -> pendingUsePortalService.issueCode(String.valueOf(created.get("orderNo")), talentId))
                .isInstanceOf(com.talentcard.server.common.BusinessException.class);
    }
 
    @Test
    void talentCodeRejectsMerchantWithoutApplicableStoreConsumptionRule() {
        jdbc.update("delete from merchant_store_consume_rule where merchant_id = ?", merchantId);
 
        assertThatThrownBy(() -> verificationService.scan(principal, new VerificationScanRequest(token)))
                .isInstanceOf(com.talentcard.server.common.BusinessException.class)
                .hasMessageContaining("未配置适用于该人才卡的到店消费权益");
    }
 
    @Test
    void talentCodeStoreConsumptionUsesConfiguredCouponAfterTalentDiscount() {
        Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(token));
        String verificationId = String.valueOf(scanned.get("verificationId"));
        String couponName = "Store coupon " + System.nanoTime();
        jdbc.update("""
                insert into merchant_coupon(merchant_id, coupon_name, condition_type, condition_amount, coupon_amount,
                    talent_levels, store_consume, status, deleted)
                values (?, ?, 'FULL', 100.00, 50.00, 'EXCELLENT', 1, 1, 0)
                """, merchantId, couponName);
        Long couponId = jdbc.queryForObject("select id from merchant_coupon where merchant_id = ? and coupon_name = ?", Long.class, merchantId, couponName);
 
        Map<String, Object> detail = manualConsumptionService.detail(principal, verificationId);
        String ruleId = String.valueOf(((java.util.List<?>) detail.get("rules")).get(0) instanceof Map<?, ?> rule ? rule.get("id") : "");
        assertThat((java.util.List<?>) detail.get("coupons")).isNotEmpty();
 
        ManualConsumptionCreateRequest belowDiscountedThreshold = new ManualConsumptionCreateRequest(verificationId, Long.valueOf(ruleId), new BigDecimal("110.00"), couponId);
        assertThatThrownBy(() -> manualConsumptionService.quote(principal, belowDiscountedThreshold))
                .isInstanceOf(com.talentcard.server.common.BusinessException.class);
 
        ManualConsumptionCreateRequest eligible = new ManualConsumptionCreateRequest(verificationId, Long.valueOf(ruleId), new BigDecimal("120.00"), couponId);
        Map<String, Object> quote = manualConsumptionService.quote(principal, eligible);
        assertThat(quote).containsEntry("couponAmount", 5000L).containsEntry("payAmount", 5800L).containsEntry("subsidyAmount", 6200L);
        Map<String, Object> created = manualConsumptionService.create(principal, eligible, "manual-coupon-" + System.nanoTime());
        assertThat(jdbc.queryForObject("select coupon_id from benefit_order where order_no = ?", Long.class, created.get("orderNo"))).isEqualTo(couponId);
    }
 
    @Test
    void pendingOrderCanBeCancelledAfterPrepayIsCreated() {
        String suffix = String.valueOf(System.nanoTime());
        String orderNo = "PREPAY-CANCEL-" + suffix;
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, card_id, merchant_id, store_id, order_type,
                                          order_amount, subsidy_amount, pay_amount, order_status, verify_status, deleted)
                values (?, ?, ?, ?, ?, 'GROUP_BUY', 10000, 0, 10000, 0, 0, 0)
                """, orderNo, talentId, cardId, merchantId, storeId);
        Long orderId = jdbc.queryForObject("select max(id) from benefit_order", Long.class);
        jdbc.update("""
                insert into payment_transaction(payment_no, provider, order_id, order_no, talent_id, merchant_id,
                                                amount, status, idempotency_key)
                values (?, 'HUIFU', ?, ?, ?, ?, 10000, 'PREPAY_CREATED', ?)
                """, "PREPAY-CANCEL-PAY-" + suffix, orderId, orderNo, talentId, merchantId, "prepay-cancel-" + suffix);
 
        pendingOrderPortalService.cancel(orderNo, talentId);
 
        assertThat(jdbc.queryForObject("select order_status from benefit_order where id = ?", Integer.class, orderId)).isEqualTo(2);
        assertThat(jdbc.queryForObject("select status from payment_transaction where order_id = ?", String.class, orderId)).isEqualTo("CLOSED");
        assertThat(jdbc.queryForObject("select closed_at is not null from payment_transaction where order_id = ?", Boolean.class, orderId)).isTrue();
    }
 
    @Test
    void orderVerificationCodeIsRecognizedAndConsumesThePaidOrderOnce() {
        String suffix = String.valueOf(System.nanoTime());
        String orderNo = "ORDER-VERIFY-" + suffix;
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, card_id, merchant_id, store_id, order_type,
                                          order_amount, subsidy_amount, pay_amount, order_status, verify_status, deleted)
                values (?, ?, ?, ?, ?, 'GROUP_BUY', 10000, 2000, 8000, 1, 0, 0)
                """, orderNo, talentId, cardId, merchantId, storeId);
        Long orderId = jdbc.queryForObject("select max(id) from benefit_order", Long.class);
        String orderToken = "order-code-" + suffix;
        jdbc.update("""
                insert into order_dynamic_verification_code(code_token, display_code, order_id, talent_id, expires_at, status)
                values (?, '123456', ?, ?, ?, 0)
                """, orderToken, orderId, talentId, Timestamp.valueOf(LocalDateTime.now().plusMinutes(1)));
 
        Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(orderToken));
        Map<String, Object> confirmed = verificationService.confirm(principal,
                new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "order-code-key");
        Map<String, Object> repeated = verificationService.confirm(principal,
                new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "order-code-key");
 
        assertThat(scanned.get("credentialType")).isEqualTo("ORDER_CODE");
        assertThat(confirmed.get("verified")).isEqualTo(true);
        assertThat(repeated.get("verifyNo")).isEqualTo(confirmed.get("verifyNo"));
        assertThat(jdbc.queryForObject("select status from order_dynamic_verification_code where code_token = ?", Integer.class, orderToken)).isEqualTo(1);
        assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, orderId)).isEqualTo(1);
        assertThat(jdbc.queryForObject("select count(*) from verify_record where order_id = ? and verify_type = 'ORDER_CODE'", Integer.class, orderId)).isEqualTo(1);
    }
 
    @Test
    void freeStayCodeIsRecognizedAndConsumesOneVoucherUseOnce() {
        String suffix = String.valueOf(System.nanoTime());
        String voucherNo = "FREE-STAY-" + suffix;
        jdbc.update("""
                insert into talent_free_stay_voucher(voucher_no, talent_id, merchant_id, card_id, total_count, used_count, valid_end, status)
                values (?, ?, ?, ?, 5, 1, ?, 1)
                """, voucherNo, talentId, merchantId, cardId, LocalDate.now().plusMonths(1));
        Long voucherId = jdbc.queryForObject("select max(id) from talent_free_stay_voucher", Long.class);
        String freeStayToken = "free-stay-code-" + suffix;
        jdbc.update("""
                insert into free_stay_verification_code(code_token, voucher_id, expires_at, status)
                values (?, ?, ?, 0)
                """, freeStayToken, voucherId, Timestamp.valueOf(LocalDateTime.now().plusMinutes(1)));
 
        Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(freeStayToken));
        Map<String, Object> confirmed = verificationService.confirm(principal,
                new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "free-stay-code-key");
        Map<String, Object> repeated = verificationService.confirm(principal,
                new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "free-stay-code-key");
 
        assertThat(scanned.get("credentialType")).isEqualTo("FREE_STAY_CODE");
        assertThat(confirmed.get("verified")).isEqualTo(true);
        assertThat(repeated.get("verifyNo")).isEqualTo(confirmed.get("verifyNo"));
        assertThat(jdbc.queryForObject("select status from free_stay_verification_code where code_token = ?", Integer.class, freeStayToken)).isEqualTo(1);
        assertThat(jdbc.queryForObject("select used_count from talent_free_stay_voucher where id = ?", Integer.class, voucherId)).isEqualTo(2);
        assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'HOTEL_FREE' and talent_id = ?", Integer.class, talentId)).isEqualTo(1);
    }
 
    @Test
    void orderAndFreeStayCodesCannotBeScannedByAnotherMerchant() {
        String suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into merchant(merchant_name, status, can_login_merchant_app, deleted) values (?, 1, 1, 0)", "Other merchant " + suffix);
        Long otherMerchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
        jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", otherMerchantId, "Other store " + suffix);
        Long otherStoreId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
        MerchantPrincipal otherPrincipal = new MerchantPrincipal(1000L, otherMerchantId, otherStoreId, "other-operator");
 
        jdbc.update("""
                insert into talent_free_stay_voucher(voucher_no, talent_id, merchant_id, card_id, total_count, used_count, valid_end, status)
                values (?, ?, ?, ?, 1, 0, ?, 1)
                """, "FREE-STAY-SCOPE-" + suffix, talentId, merchantId, cardId, LocalDate.now().plusMonths(1));
        Long voucherId = jdbc.queryForObject("select max(id) from talent_free_stay_voucher", Long.class);
        String freeStayToken = "free-stay-scope-" + suffix;
        jdbc.update("insert into free_stay_verification_code(code_token, voucher_id, expires_at, status) values (?, ?, ?, 0)",
                freeStayToken, voucherId, Timestamp.valueOf(LocalDateTime.now().plusMinutes(1)));
 
        org.assertj.core.api.Assertions.assertThatThrownBy(() -> verificationService.scan(otherPrincipal, new VerificationScanRequest(freeStayToken)))
                .isInstanceOf(com.talentcard.server.common.BusinessException.class);
        assertThat(jdbc.queryForObject("select status from free_stay_verification_code where code_token = ?", Integer.class, freeStayToken)).isZero();
    }
 
    @Test
    void freeBenefitIssueCreatesASeparateVoucherAndVerificationCode() {
        Map<String, Object> issued = freeBenefitPortalService.code(talentId, merchantId);
        String imagePath = String.valueOf(issued.get("imagePath"));
        String codeToken = imagePath.substring(imagePath.lastIndexOf('/', imagePath.length() - "/image".length() - 1) + 1,
                imagePath.length() - "/image".length());
 
        assertThat(issued.get("remainingCount")).isEqualTo(5);
        assertThat(codeToken).startsWith("FS1");
        assertThat(jdbc.queryForObject("select count(*) from talent_free_stay_voucher where talent_id = ? and merchant_id = ?", Integer.class, talentId, merchantId)).isEqualTo(1);
        assertThat(jdbc.queryForObject("select count(*) from free_stay_verification_code where code_token = ? and status = 0", Integer.class, codeToken)).isEqualTo(1);
        assertThat(freeBenefitPortalService.codeImage(codeToken)).isNotEmpty();
    }
}