package com.talentcard.server.business.merchant;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import java.sql.Timestamp;
|
import java.time.LocalDateTime;
|
import java.util.Map;
|
|
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
|
import com.talentcard.server.common.BusinessException;
|
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
class MerchantVerificationOrderCodeTests {
|
|
private JdbcTemplate jdbc;
|
private TransactionTemplate transactions;
|
private MerchantVerificationService service;
|
private com.talentcard.server.business.order.OrderVerificationService orderVerification;
|
private MerchantPrincipal principal;
|
/** 每个用例用独立的订单号与核销码号,避免共享内存库互相踩。 */
|
private static final java.util.concurrent.atomic.AtomicLong ORDER_SEQUENCE = new java.util.concurrent.atomic.AtomicLong(1000);
|
private static final java.util.concurrent.atomic.AtomicLong ORDER_CODE_SEQUENCE = new java.util.concurrent.atomic.AtomicLong(1000);
|
|
@BeforeEach
|
void setUp() {
|
DriverManagerDataSource dataSource = new DriverManagerDataSource(
|
"jdbc:h2:mem:order-code-verification-" + System.nanoTime()
|
+ ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
|
jdbc = new JdbcTemplate(dataSource);
|
transactions = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
|
jdbc.execute("""
|
create table merchant_verification_attempt (
|
id bigint auto_increment primary key,
|
verification_id varchar(64) not null,
|
credential_type varchar(32) not null,
|
dynamic_code_id bigint,
|
order_verification_code_id bigint,
|
free_stay_verification_code_id bigint,
|
merchant_id bigint not null,
|
store_id bigint not null,
|
operator_id bigint not null,
|
expires_at timestamp,
|
status tinyint not null default 0,
|
verify_no varchar(64),
|
confirmed_at timestamp
|
)
|
""");
|
jdbc.execute("""
|
create table benefit_order (
|
id bigint primary key,
|
talent_id bigint not null,
|
card_id bigint,
|
benefit_id bigint,
|
order_no varchar(64) not null,
|
order_amount bigint not null,
|
subsidy_amount bigint not null,
|
pay_amount bigint not null,
|
order_status tinyint not null,
|
verify_status tinyint not null,
|
refund_status tinyint not null default 0,
|
merchant_id bigint not null,
|
store_id bigint,
|
collect_huifu_id varchar(32),
|
deleted tinyint not null default 0
|
)
|
""");
|
jdbc.execute("""
|
create table order_dynamic_verification_code (
|
id bigint primary key,
|
order_id bigint not null,
|
expires_at timestamp not null,
|
status tinyint not null
|
)
|
""");
|
jdbc.execute("create table talent_card (id bigint primary key, card_level varchar(64), deleted tinyint not null default 0)");
|
jdbc.execute("create table benefit (id bigint primary key, benefit_name varchar(128))");
|
jdbc.execute("""
|
create table verify_record (
|
id bigint auto_increment primary key,
|
verify_no varchar(64) not null,
|
order_id bigint,
|
talent_id bigint,
|
card_id bigint,
|
benefit_id bigint,
|
merchant_id bigint,
|
store_id bigint,
|
staff_id bigint,
|
verify_type varchar(32),
|
verify_amount bigint,
|
subsidy_amount bigint,
|
remark varchar(255),
|
idempotency_key varchar(64)
|
)
|
""");
|
// 资金确认服务会按订单查支付流水,这里给出结构一致的最小表。
|
jdbc.execute("""
|
create table payment_transaction (
|
id bigint auto_increment primary key,
|
payment_no varchar(64) not null,
|
order_id bigint not null,
|
status varchar(32) not null,
|
amount bigint not null,
|
provider_huifu_id varchar(32),
|
provider_transaction_no varchar(128),
|
funds_confirmation_status varchar(16),
|
created_at timestamp not null default current_timestamp
|
)
|
""");
|
// 用真实的核销实现:错误分支(未支付/已退款)必须由真实逻辑抛出,替身会把这些分支掩盖掉。
|
com.talentcard.server.business.order.OrderVerificationService orderVerification =
|
new com.talentcard.server.business.order.OrderVerificationService(jdbc,
|
new com.talentcard.server.business.order.OrderFundBoundaryService(jdbc),
|
new com.talentcard.server.business.payment.PaymentFundConfirmationService(jdbc, transactions,
|
org.mockito.Mockito.mock(com.talentcard.server.business.payment.huifu.HuifuDelayAccountGateway.class)));
|
service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc, null), orderVerification);
|
this.orderVerification = orderVerification;
|
principal = new MerchantPrincipal(9L, 88L, 7L, "operator");
|
}
|
|
/**
|
* 订单核销码过期后不再接受直接核销:持码人重新扫码即可(扫码会刷新码)。
|
*
|
* <p>此前的实现容忍过期,把已经失效的扫码码也当作可核销;现在统一由订单核销入口处理,
|
* 过期就明确拒绝并把这次扫码尝试标记为失效,避免"拿着旧码也能核销"的模糊状态。
|
*/
|
@Test
|
void rejectsGroupOrderWhenTheScannedCodeHasExpired() {
|
insertOrder("expired-group-order", 1);
|
Long expiredOrderId = jdbc.queryForObject("select max(id) from benefit_order", Long.class);
|
|
assertThatThrownBy(() -> confirm("expired-group-order"))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("失效");
|
assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, expiredOrderId)).isZero();
|
assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class,
|
"expired-group-order")).isZero();
|
}
|
|
@Test
|
void stillRejectsUnpaidGroupOrder() {
|
insertOrder("unpaid-group-order", 0);
|
|
assertThatThrownBy(() -> confirm("unpaid-group-order"))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("未支付");
|
}
|
|
@Test
|
void rejectsGroupOrderThatWasRefundedWhileTheCodeWasOpen() {
|
insertOrder("refunded-group-order", 1);
|
Long refundedOrderId = jdbc.queryForObject("select max(id) from benefit_order", Long.class);
|
jdbc.update("update benefit_order set refund_status = 2 where id = ?", refundedOrderId);
|
|
assertThatThrownBy(() -> confirm("refunded-group-order"))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("退款");
|
assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, refundedOrderId)).isZero();
|
}
|
|
/** 未过期的订单核销码可以正常核销:置订单已核销、写核销记录、标记扫码尝试完成。 */
|
@Test
|
void confirmsGroupOrderWithAFreshScannedCode() {
|
insertOrder("fresh-group-order", 1, LocalDateTime.now().plusMinutes(5));
|
|
Map<String, Object> confirmed = confirm("fresh-group-order");
|
long orderId = ((Number) confirmed.get("orderId")).longValue();
|
|
assertThat(confirmed.get("verified")).isEqualTo(true);
|
assertThat(jdbc.queryForObject("select status from order_dynamic_verification_code where order_id = ?", Integer.class, orderId)).isEqualTo(1);
|
assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = ?", Integer.class, orderId)).isEqualTo(1);
|
assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class,
|
"fresh-group-order")).isEqualTo(1);
|
assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'ORDER_CODE' and order_id = ?", Integer.class, orderId)).isEqualTo(1);
|
}
|
|
/**
|
* 订单核销按订单行锁串行化,所以测试也必须在一个真实事务里调用它。
|
*
|
* <p>订单核销不再走 {@code verification/confirm},改为调统一的订单核销实现,
|
* 并把扫码尝试编号带过去标记尝试完成。
|
*/
|
private Map<String, Object> confirm(String verificationId) {
|
return transactions.execute(status -> {
|
Map<String, Object> attempt = jdbc.queryForMap(
|
"select a.id, a.expires_at, c.order_id from merchant_verification_attempt a"
|
+ " join order_dynamic_verification_code c on c.id = a.order_verification_code_id"
|
+ " where a.verification_id = ?", verificationId);
|
long attemptId = ((Number) attempt.get("id")).longValue();
|
long orderId = ((Number) attempt.get("order_id")).longValue();
|
java.time.LocalDateTime expiresAt = ((Timestamp) attempt.get("expires_at")).toLocalDateTime();
|
String verifyNo = orderVerification.verifyByMerchant(principal.merchantId(), principal.storeId(),
|
principal.operatorId(), orderId, "ORDER_CODE", 8000L, 2000L, "订单动态核销码",
|
attemptId, "order-confirm-" + verificationId, expiresAt);
|
return Map.of("verified", !verifyNo.isBlank(), "verifyNo", verifyNo, "orderId", orderId);
|
});
|
}
|
|
private void insertOrder(String verificationId, int orderStatus) {
|
insertOrder(verificationId, orderStatus, LocalDateTime.now().minusMinutes(1));
|
}
|
|
private void insertOrder(String verificationId, int orderStatus, LocalDateTime scanExpiresAt) {
|
// 每个用例用独立订单号:共用同一个 id 时,前一个用例核销过的订单会让后一个用例直接失败。
|
long orderId = ORDER_SEQUENCE.incrementAndGet();
|
jdbc.update("insert into talent_card(id, card_level, deleted) values (10, '卓越人才卡', 0)");
|
jdbc.update("""
|
insert into benefit_order(id, talent_id, card_id, benefit_id, order_no, order_amount, subsidy_amount,
|
pay_amount, order_status, verify_status, merchant_id, store_id, deleted)
|
values (?, 1015, 10, null, ?, 10000, 3000, 7000, ?, 0, 88, 7, 0)
|
""", orderId, "GROUP-" + verificationId, orderStatus);
|
jdbc.update("""
|
insert into order_dynamic_verification_code(id, order_id, expires_at, status)
|
values (?, ?, ?, 2)
|
""", ORDER_CODE_SEQUENCE.incrementAndGet(), orderId, Timestamp.valueOf(scanExpiresAt));
|
Long codeId = jdbc.queryForObject("select max(id) from order_dynamic_verification_code", Long.class);
|
jdbc.update("""
|
insert into merchant_verification_attempt(verification_id, credential_type, order_verification_code_id,
|
merchant_id, store_id, operator_id, expires_at, status)
|
values (?, 'ORDER_CODE', ?, 88, 7, 9, ?, 0)
|
""", verificationId, codeId, Timestamp.valueOf(scanExpiresAt));
|
}
|
}
|