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"); } /** * 订单核销码过期后不再接受直接核销:持码人重新扫码即可(扫码会刷新码)。 * *
此前的实现容忍过期,把已经失效的扫码码也当作可核销;现在统一由订单核销入口处理,
* 过期就明确拒绝并把这次扫码尝试标记为失效,避免"拿着旧码也能核销"的模糊状态。
*/
@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 订单核销不再走 {@code verification/confirm},改为调统一的订单核销实现,
* 并把扫码尝试编号带过去标记尝试完成。
*/
private Map