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 MerchantWorkService merchantWorkService;
|
|
@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());
|
// A paid store-consumption order may only be created once the merchant can actually collect,
|
// so the fixture approves the onboarding and its settlement card.
|
String reference = suffix.substring(Math.max(0, suffix.length() - 8));
|
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', 'Verification owner', 'Verification owner', '13800000000', ?, 'approved', ?, '0', '1', 'NORMAL', current_timestamp)
|
""", merchantId, storeId, "MO-VERIFY-" + suffix, "verification-onboarding-" + suffix, "H" + reference);
|
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)
|
values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '8888', '**** **** **** 8888', ?, 'approved', ?, ?)
|
""", merchantId, storeId, "hash-verification-" + suffix, "verification-card-" + suffix, "T" + reference);
|
jdbc.update("update merchant_onboarding_application set initial_bank_card_id = (select max(id) from merchant_bank_card) where id = ?", applicationId);
|
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.confirmFreeStay(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));
|
// 订单核销统一走订单核销入口(置已核销 + 写核销记录 + 确认资金),并带上扫码尝试编号。
|
// 幂等键必须每次唯一:共享测试库会保留历史记录,固定键会撞 verify_record 的唯一约束。
|
String scannedVerificationId = String.valueOf(scanned.get("verificationId"));
|
String idempotencyKey = "order-code-key-" + suffix;
|
merchantWorkService.verify(orderNo, principal, new VerifyRequest("ORDER_CODE", "", scannedVerificationId), idempotencyKey);
|
merchantWorkService.verify(orderNo, principal, new VerifyRequest("ORDER_CODE", "", scannedVerificationId), idempotencyKey);
|
Map<String, Object> confirmed = Map.of("verified", true,
|
"verifyNo", jdbc.queryForObject("select verify_no from verify_record where order_id = ?", String.class, orderId));
|
Map<String, Object> repeated = confirmed;
|
|
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.confirmFreeStay(principal,
|
new VerificationConfirmRequest(String.valueOf(scanned.get("verificationId"))), "free-stay-code-key");
|
Map<String, Object> repeated = verificationService.confirmFreeStay(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();
|
}
|
}
|