fix: allow scanned benefit codes to finish verification
| | |
| | | if (number(attempt.get("attemptStatus")) == 1) { |
| | | return confirmed(attempt.get("verifyNo"), null); |
| | | } |
| | | if (number(attempt.get("attemptStatus")) != 0 || expired(attempt.get("expiresAt"))) { |
| | | String credentialType = String.valueOf(attempt.get("credentialType")); |
| | | boolean codeExpiryRequired = !ORDER_CODE.equals(credentialType) && !FREE_STAY_CODE.equals(credentialType); |
| | | if (number(attempt.get("attemptStatus")) != 0 || (codeExpiryRequired && expired(attempt.get("expiresAt")))) { |
| | | jdbc.update("update merchant_verification_attempt set status = 2 where id = ? and status = 0", number(attempt.get("attemptId"))); |
| | | throw new BusinessException(ErrorCode.CONFLICT, "核销凭证已失效,请重新扫码"); |
| | | } |
| | | String credentialType = String.valueOf(attempt.get("credentialType")); |
| | | return switch (credentialType) { |
| | | case TALENT_CODE -> throw new BusinessException(ErrorCode.CONFLICT, "人才码需要先录入消费金额并发送订单"); |
| | | case ORDER_CODE -> confirmOrderCode(principal, attempt); |
| | |
| | | from order_dynamic_verification_code c |
| | | join benefit_order o on o.id = c.order_id and o.deleted = 0 |
| | | left join talent_card tc on tc.id = o.card_id and tc.deleted = 0 |
| | | where c.id = ? and c.status = 0 |
| | | where c.id = ? and c.status in (0, 2) |
| | | limit 1 for update |
| | | """, number(attempt.get("orderVerificationCodeId"))); |
| | | if (codes.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "订单核销码已被使用或已失效"); |
| | | } |
| | | Map<String, Object> code = codes.get(0); |
| | | if (!belongsToPrincipal(code, principal) || number(code.get("orderStatus")) != 1 || number(code.get("verifyStatus")) != 0 || expired(code.get("expiresAt"))) { |
| | | if (!belongsToPrincipal(code, principal) || number(code.get("orderStatus")) != 1 || number(code.get("verifyStatus")) != 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可核销"); |
| | | } |
| | | int codeUpdated = jdbc.update(""" |
| | | update order_dynamic_verification_code set status = 1 |
| | | where id = ? and status = 0 and expires_at > now() |
| | | where id = ? and status in (0, 2) |
| | | """, number(code.get("orderVerificationCodeId"))); |
| | | int orderUpdated = jdbc.update(""" |
| | | update benefit_order set verify_status = 1 |
| | |
| | | from free_stay_verification_code c |
| | | join talent_free_stay_voucher v on v.id = c.voucher_id |
| | | left join talent_card tc on tc.id = v.card_id and tc.deleted = 0 |
| | | where c.id = ? and c.status = 0 |
| | | where c.id = ? and c.status in (0, 2) |
| | | limit 1 for update |
| | | """, number(attempt.get("freeStayVerificationCodeId"))); |
| | | if (codes.isEmpty()) { |
| | |
| | | } |
| | | Map<String, Object> code = codes.get(0); |
| | | if (!principal.merchantId().equals(number(code.get("merchantId"))) || number(code.get("voucherStatus")) != 1 |
| | | || expiredDate(code.get("validEnd")) || number(code.get("usedCount")) >= number(code.get("totalCount")) || expired(code.get("expiresAt"))) { |
| | | || expiredDate(code.get("validEnd")) || number(code.get("usedCount")) >= number(code.get("totalCount"))) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "免费入住券当前不可核销"); |
| | | } |
| | | int codeUpdated = jdbc.update(""" |
| | | update free_stay_verification_code set status = 1 |
| | | where id = ? and status = 0 and expires_at > now() |
| | | where id = ? and status in (0, 2) |
| | | """, number(code.get("freeStayVerificationCodeId"))); |
| | | int voucherUpdated = jdbc.update(""" |
| | | update talent_free_stay_voucher set used_count = used_count + 1 |
| New file |
| | |
| | | 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.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | 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.DriverManagerDataSource; |
| | | |
| | | class MerchantVerificationFreeStayTests { |
| | | |
| | | private JdbcTemplate jdbc; |
| | | private MerchantVerificationService service; |
| | | private MerchantPrincipal principal; |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | DriverManagerDataSource dataSource = new DriverManagerDataSource( |
| | | "jdbc:h2:mem:free-stay-verification-" + System.nanoTime() |
| | | + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", ""); |
| | | jdbc = new JdbcTemplate(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 talent_free_stay_voucher ( |
| | | id bigint primary key, |
| | | talent_id bigint not null, |
| | | merchant_id bigint not null, |
| | | card_id bigint not null, |
| | | total_count int not null, |
| | | used_count int not null, |
| | | valid_end date not null, |
| | | status tinyint not null |
| | | ) |
| | | """); |
| | | jdbc.execute(""" |
| | | create table free_stay_verification_code ( |
| | | id bigint primary key, |
| | | voucher_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 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) |
| | | ) |
| | | """); |
| | | service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc)); |
| | | principal = new MerchantPrincipal(9L, 88L, 7L, "operator"); |
| | | } |
| | | |
| | | @Test |
| | | void confirmsFreeStayAfterScannedCodeExpiresAndRefreshes() { |
| | | insertFreeStay("free-stay-expired-code", LocalDate.now().plusDays(1)); |
| | | |
| | | MapResult confirmed = confirm("free-stay-expired-code"); |
| | | |
| | | assertThat(confirmed.verified()).isTrue(); |
| | | assertThat(jdbc.queryForObject("select status from free_stay_verification_code where id = 30", Integer.class)).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select used_count from talent_free_stay_voucher where id = 20", Integer.class)).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class, |
| | | "free-stay-expired-code")).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'HOTEL_FREE'", Integer.class)).isEqualTo(1); |
| | | } |
| | | |
| | | @Test |
| | | void stillRejectsExpiredFreeStayVoucher() { |
| | | insertFreeStay("free-stay-expired-voucher", LocalDate.now().minusDays(1)); |
| | | |
| | | assertThatThrownBy(() -> confirm("free-stay-expired-voucher")) |
| | | .isInstanceOf(BusinessException.class) |
| | | .hasMessageContaining("当前不可核销"); |
| | | } |
| | | |
| | | private void insertFreeStay(String verificationId, LocalDate voucherValidEnd) { |
| | | jdbc.update("insert into talent_card(id, card_level, deleted) values (10, '卓越人才卡', 0)"); |
| | | jdbc.update(""" |
| | | insert into talent_free_stay_voucher(id, talent_id, merchant_id, card_id, total_count, used_count, valid_end, status) |
| | | values (20, 1015, 88, 10, 2, 0, ?, 1) |
| | | """, voucherValidEnd); |
| | | jdbc.update(""" |
| | | insert into free_stay_verification_code(id, voucher_id, expires_at, status) |
| | | values (30, 20, ?, 2) |
| | | """, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1))); |
| | | jdbc.update(""" |
| | | insert into merchant_verification_attempt(verification_id, credential_type, free_stay_verification_code_id, |
| | | merchant_id, store_id, operator_id, expires_at, status) |
| | | values (?, 'FREE_STAY_CODE', 30, 88, 7, 9, ?, 0) |
| | | """, verificationId, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1))); |
| | | } |
| | | |
| | | private MapResult confirm(String verificationId) { |
| | | java.util.Map<String, Object> result = service.confirm(principal, new VerificationConfirmRequest(verificationId), |
| | | "free-stay-confirm-" + verificationId); |
| | | return new MapResult(Boolean.TRUE.equals(result.get("verified"))); |
| | | } |
| | | |
| | | private record MapResult(boolean verified) {} |
| | | } |
| New file |
| | |
| | | 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.DriverManagerDataSource; |
| | | |
| | | class MerchantVerificationOrderCodeTests { |
| | | |
| | | private JdbcTemplate jdbc; |
| | | private MerchantVerificationService service; |
| | | private MerchantPrincipal principal; |
| | | |
| | | @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); |
| | | 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, |
| | | merchant_id bigint not null, |
| | | store_id bigint, |
| | | 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) |
| | | ) |
| | | """); |
| | | service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc)); |
| | | principal = new MerchantPrincipal(9L, 88L, 7L, "operator"); |
| | | } |
| | | |
| | | @Test |
| | | void confirmsGroupOrderAfterScannedCodeExpiresAndRefreshes() { |
| | | insertOrder("expired-group-order", 1); |
| | | |
| | | Map<String, Object> confirmed = service.confirm(principal, |
| | | new VerificationConfirmRequest("expired-group-order"), "order-confirm-expired-group-order"); |
| | | |
| | | assertThat(confirmed.get("verified")).isEqualTo(true); |
| | | assertThat(jdbc.queryForObject("select status from order_dynamic_verification_code where id = 30", Integer.class)).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = 20", Integer.class)).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class, |
| | | "expired-group-order")).isEqualTo(1); |
| | | assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'ORDER_CODE'", Integer.class)).isEqualTo(1); |
| | | } |
| | | |
| | | @Test |
| | | void stillRejectsUnpaidGroupOrder() { |
| | | insertOrder("unpaid-group-order", 0); |
| | | |
| | | assertThatThrownBy(() -> service.confirm(principal, new VerificationConfirmRequest("unpaid-group-order"), |
| | | "order-confirm-unpaid-group-order")) |
| | | .isInstanceOf(BusinessException.class) |
| | | .hasMessageContaining("当前不可核销"); |
| | | } |
| | | |
| | | private void insertOrder(String verificationId, int orderStatus) { |
| | | 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 (20, 1015, 10, null, ?, 10000, 3000, 7000, ?, 0, 88, 7, 0) |
| | | """, "GROUP-" + verificationId, orderStatus); |
| | | jdbc.update(""" |
| | | insert into order_dynamic_verification_code(id, order_id, expires_at, status) |
| | | values (30, 20, ?, 2) |
| | | """, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1))); |
| | | 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', 30, 88, 7, 9, ?, 0) |
| | | """, verificationId, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1))); |
| | | } |
| | | } |