diff --git a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java index 550b748..f48a933 100644 --- a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java @@ -169,28 +169,22 @@ public class WalletPrerequisiteProjectionService { (source.userId + "|" + institution.bankCode).getBytes(StandardCharsets.UTF_8)); List existing = bankAccounts(source.userId, institution.bankCode); if (!existing.isEmpty()) { - validateAccount(existing.get(0), source, institution); - BigDecimal expectedBalance = source.application.accountBalance != null - && source.application.accountBalance.compareTo(BigDecimal.ZERO) > 0 - ? source.application.accountBalance : new BigDecimal("50000.00"); - jdbc.update("UPDATE simulated_bank_account SET balance=? WHERE account_id=? AND balance=0", - expectedBalance, existing.get(0).accountId); + restoreUnusedBankAccount(existing.get(0), source, institution); + validateAccount(bankAccounts(source.userId, institution.bankCode).get(0), source, institution); return existing.get(0).accountId; } - BigDecimal initialBalance = source.application.accountBalance != null - && source.application.accountBalance.compareTo(BigDecimal.ZERO) > 0 - ? source.application.accountBalance : new BigDecimal("50000.00"); try { jdbc.update("INSERT INTO simulated_bank_account " + "(account_id,user_id,bank_code,bank_name,card_number,card_last4,balance,frozen_amount,status,created_at,updated_at) " + "VALUES (?,?,?,?,?,?,?,0,'ACTIVE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", expectedId, source.userId, institution.bankCode, source.application.bankName, source.application.cardNumber, lastFour(source.application.cardNumber), - initialBalance); + source.application.accountBalance); } catch (DuplicateKeyException exception) { existing = bankAccounts(source.userId, institution.bankCode); if (existing.isEmpty()) throw exception; - validateAccount(existing.get(0), source, institution); + restoreUnusedBankAccount(existing.get(0), source, institution); + validateAccount(bankAccounts(source.userId, institution.bankCode).get(0), source, institution); return existing.get(0).accountId; } return expectedId; @@ -299,10 +293,31 @@ public class WalletPrerequisiteProjectionService { } private List bankAccounts(String userId, String bankCode) { - return jdbc.query("SELECT account_id,user_id,bank_code,card_number FROM simulated_bank_account " + - "WHERE user_id=? AND bank_code=?", + return jdbc.query("SELECT account_id,user_id,bank_code,card_number,bank_name,balance,frozen_amount,status " + + "FROM simulated_bank_account WHERE user_id=? AND bank_code=? FOR UPDATE", (rs, row) -> new BankAccount(rs.getString("account_id"), rs.getString("user_id"), - rs.getString("bank_code"), rs.getString("card_number")), userId, bankCode); + rs.getString("bank_code"), rs.getString("card_number"), + blank(rs.getString("bank_name")) && rs.getBigDecimal("balance").signum() == 0 + && rs.getBigDecimal("frozen_amount").signum() == 0 + && "ACTIVE".equals(rs.getString("status"))), userId, bankCode); + } + + private void restoreUnusedBankAccount(BankAccount value, SourceWallet source, Institution institution) { + if (!value.emptyShell || !source.userId.equals(value.userId) + || !institution.bankCode.equals(value.bankCode) || blank(source.application.bankName) + || source.application.accountBalance.signum() < 0) { + return; + } + // Repair only unused legacy shells. Runtime balances must never be reset from an opening snapshot. + jdbc.update("UPDATE simulated_bank_account SET bank_name=?,card_number=?,card_last4=?,balance=?," + + "updated_at=CURRENT_TIMESTAMP WHERE account_id=? AND user_id=? AND bank_code=? " + + "AND TRIM(COALESCE(bank_name,''))='' AND balance=0 AND frozen_amount=0 AND status='ACTIVE' " + + "AND NOT EXISTS (SELECT 1 FROM currency_exchange_order WHERE bank_account_id=?) " + + "AND NOT EXISTS (SELECT 1 FROM exchange_debit_record WHERE bank_account_id=?) " + + "AND NOT EXISTS (SELECT 1 FROM wallet_bank_binding WHERE bank_account_id=? AND wallet_id<>?)", + source.application.bankName, source.application.cardNumber, lastFour(source.application.cardNumber), + source.application.accountBalance, value.accountId, source.userId, institution.bankCode, + value.accountId, value.accountId, value.accountId, source.walletId); } private void validateAccount(BankAccount value, SourceWallet source, Institution institution) { @@ -442,9 +457,11 @@ public class WalletPrerequisiteProjectionService { private final String userId; private final String bankCode; private final String cardNumber; - private BankAccount(String accountId, String userId, String bankCode, String cardNumber) { + private final boolean emptyShell; + private BankAccount(String accountId, String userId, String bankCode, String cardNumber, boolean emptyShell) { this.accountId = accountId; this.userId = userId; this.bankCode = bankCode; this.cardNumber = cardNumber; + this.emptyShell = emptyShell; } } diff --git a/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetResult.java b/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetResult.java new file mode 100644 index 0000000..222d56c --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetResult.java @@ -0,0 +1,24 @@ +package com.yau.digitalrmb.walletopening.application; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel(description = "开通数字货币钱包实验重新实训结果") +public class WalletOpeningTrainingResetResult { + @ApiModelProperty(value = "是否重置成功", example = "true") + private final boolean reset; + @ApiModelProperty(value = "逻辑删除的记录总数", example = "12") + private final int deletedRecordCount; + @ApiModelProperty(value = "处理说明") + private final String message; + + public WalletOpeningTrainingResetResult(int deletedRecordCount) { + this.reset = true; + this.deletedRecordCount = deletedRecordCount; + this.message = "钱包开通实验步骤数据已逻辑删除,可从步骤一重新开始"; + } + + public boolean isReset() { return reset; } + public int getDeletedRecordCount() { return deletedRecordCount; } + public String getMessage() { return message; } +} diff --git a/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetService.java b/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetService.java new file mode 100644 index 0000000..d5896f5 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/walletopening/application/WalletOpeningTrainingResetService.java @@ -0,0 +1,126 @@ +package com.yau.digitalrmb.walletopening.application; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import com.yau.digitalrmb.walletopening.infrastructure.BankReceivedApplicationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.BankReceivedApplicationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.CentralWalletActivationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.CentralWalletActivationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.CentralWalletRegistrationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.CentralWalletRegistrationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.SmartContractGenerationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.SmartContractGenerationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.WalletApplicationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.WalletApplicationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.WalletIdentifierGenerationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.WalletIdentifierGenerationMapper; +import com.yau.digitalrmb.walletopening.infrastructure.WalletRegistrationRequestEntity; +import com.yau.digitalrmb.walletopening.infrastructure.WalletRegistrationRequestMapper; +import com.yau.digitalrmb.walletopening.infrastructure.WalletVerificationEntity; +import com.yau.digitalrmb.walletopening.infrastructure.WalletVerificationMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +@Service +public class WalletOpeningTrainingResetService { + + @Resource private WalletApplicationMapper walletApplicationMapper; + @Resource private BankReceivedApplicationMapper bankReceivedApplicationMapper; + @Resource private WalletIdentifierGenerationMapper walletIdentifierGenerationMapper; + @Resource private SmartContractGenerationMapper smartContractGenerationMapper; + @Resource private WalletRegistrationRequestMapper walletRegistrationRequestMapper; + @Resource private WalletVerificationMapper walletVerificationMapper; + @Resource private CentralWalletRegistrationMapper centralWalletRegistrationMapper; + @Resource private CentralWalletActivationMapper centralWalletActivationMapper; + @Resource private StuModuleScoreDetailsMapper scoreDetailsMapper; + + @Transactional + public WalletOpeningTrainingResetResult reset(InstitutionKeySubject subject) { + int count = 0; + count += logicalDeleteWalletApplications(subject); + count += logicalDeleteBankReceivedApplications(subject); + count += logicalDeleteWalletIdentifierGenerations(subject); + count += logicalDeleteSmartContracts(subject); + count += logicalDeleteWalletRegistrationRequests(subject); + count += logicalDeleteWalletVerifications(subject); + count += logicalDeleteCentralWalletRegistrations(subject); + count += logicalDeleteCentralWalletActivations(subject); + count += logicalDeleteScore(subject.getUserId()); + return new WalletOpeningTrainingResetResult(count); + } + + private int logicalDeleteWalletApplications(InstitutionKeySubject s) { + return walletApplicationMapper.update(null, new LambdaUpdateWrapper() + .eq(WalletApplicationEntity::getUserId, s.getUserId()) + .eq(WalletApplicationEntity::getSchoolId, s.getSchoolId()) + .eq(WalletApplicationEntity::getClassId, s.getClassId()) + .set(WalletApplicationEntity::getDeleted, true)); + } + + private int logicalDeleteBankReceivedApplications(InstitutionKeySubject s) { + return bankReceivedApplicationMapper.update(null, new LambdaUpdateWrapper() + .eq(BankReceivedApplicationEntity::getUserId, s.getUserId()) + .eq(BankReceivedApplicationEntity::getSchoolId, s.getSchoolId()) + .eq(BankReceivedApplicationEntity::getClassId, s.getClassId()) + .set(BankReceivedApplicationEntity::getDeleted, true)); + } + + private int logicalDeleteWalletIdentifierGenerations(InstitutionKeySubject s) { + return walletIdentifierGenerationMapper.update(null, new LambdaUpdateWrapper() + .eq(WalletIdentifierGenerationEntity::getUserId, s.getUserId()) + .eq(WalletIdentifierGenerationEntity::getSchoolId, s.getSchoolId()) + .eq(WalletIdentifierGenerationEntity::getClassId, s.getClassId()) + .set(WalletIdentifierGenerationEntity::getDeleted, true)); + } + + private int logicalDeleteSmartContracts(InstitutionKeySubject s) { + return smartContractGenerationMapper.update(null, new LambdaUpdateWrapper() + .eq(SmartContractGenerationEntity::getUserId, s.getUserId()) + .eq(SmartContractGenerationEntity::getSchoolId, s.getSchoolId()) + .eq(SmartContractGenerationEntity::getClassId, s.getClassId()) + .set(SmartContractGenerationEntity::getDeleted, true)); + } + + private int logicalDeleteWalletRegistrationRequests(InstitutionKeySubject s) { + return walletRegistrationRequestMapper.update(null, new LambdaUpdateWrapper() + .eq(WalletRegistrationRequestEntity::getUserId, s.getUserId()) + .eq(WalletRegistrationRequestEntity::getSchoolId, s.getSchoolId()) + .eq(WalletRegistrationRequestEntity::getClassId, s.getClassId()) + .set(WalletRegistrationRequestEntity::getDeleted, true)); + } + + private int logicalDeleteWalletVerifications(InstitutionKeySubject s) { + return walletVerificationMapper.update(null, new LambdaUpdateWrapper() + .eq(WalletVerificationEntity::getUserId, s.getUserId()) + .eq(WalletVerificationEntity::getSchoolId, s.getSchoolId()) + .eq(WalletVerificationEntity::getClassId, s.getClassId()) + .set(WalletVerificationEntity::getDeleted, true)); + } + + private int logicalDeleteCentralWalletRegistrations(InstitutionKeySubject s) { + return centralWalletRegistrationMapper.update(null, new LambdaUpdateWrapper() + .eq(CentralWalletRegistrationEntity::getUserId, s.getUserId()) + .eq(CentralWalletRegistrationEntity::getSchoolId, s.getSchoolId()) + .eq(CentralWalletRegistrationEntity::getClassId, s.getClassId()) + .set(CentralWalletRegistrationEntity::getDeleted, true)); + } + + private int logicalDeleteCentralWalletActivations(InstitutionKeySubject s) { + return centralWalletActivationMapper.update(null, new LambdaUpdateWrapper() + .eq(CentralWalletActivationEntity::getUserId, s.getUserId()) + .eq(CentralWalletActivationEntity::getSchoolId, s.getSchoolId()) + .eq(CentralWalletActivationEntity::getClassId, s.getClassId()) + .set(CentralWalletActivationEntity::getDeleted, true)); + } + + private int logicalDeleteScore(String userId) { + return scoreDetailsMapper.update(null, new LambdaUpdateWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, userId) + .eq(StuModuleScoreDetailsEntity::getMoudule, WalletOpeningScoreService.MODULE_NAME) + .set(StuModuleScoreDetailsEntity::getDeleted, true)); + } +} diff --git a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/WalletOpeningTrainingResetController.java b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/WalletOpeningTrainingResetController.java new file mode 100644 index 0000000..c36020d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/WalletOpeningTrainingResetController.java @@ -0,0 +1,38 @@ +package com.yau.digitalrmb.walletopening.interfaces.rest; + +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.security.context.AuthContextHolder; +import com.yau.digitalrmb.security.context.JwtUser; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import com.yau.digitalrmb.walletopening.application.WalletOpeningTrainingResetResult; +import com.yau.digitalrmb.walletopening.application.WalletOpeningTrainingResetService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/api/v1/wallet-opening/training") +@Tag(name = "开通数字货币钱包-实验管理", description = "钱包开通实验重新实训") +public class WalletOpeningTrainingResetController { + + @Resource + private WalletOpeningTrainingResetService resetService; + + @PostMapping("/reset") + @Operation(summary = "重新开始钱包开通实验", + description = "逻辑删除当前用户在钱包开通模块的全部步骤数据(申请记录、钱包标识、智能合约、注册登记、验证记录、央行注册与激活、成绩明细),可从步骤一重新开始。") + public ApiResponse reset() { + JwtUser jwtUser = AuthContextHolder.get(); + return ApiResponse.success(resetService.reset(InstitutionKeySubject.from(jwtUser)), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java index 38efd22..e56d428 100644 --- a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java @@ -3,11 +3,16 @@ package com.yau.digitalrmb.shared.wallet; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair; +import com.yau.digitalrmb.exchange.domain.model.ExchangeActor; +import com.yau.digitalrmb.exchange.domain.model.ExchangeContext; +import com.yau.digitalrmb.exchange.domain.repository.ExchangeResourceRepository; import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.testsupport.WalletOpeningTestData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; @@ -33,6 +38,7 @@ class WalletPrerequisiteProjectionServiceTest { @Autowired private JdbcTemplate jdbc; @Autowired private InstitutionIdentityCryptography cryptography; @Autowired private WalletPrerequisiteProjectionService service; + @Autowired private ExchangeResourceRepository exchangeResources; private InstitutionSm2KeyPair keyPair; @@ -109,6 +115,7 @@ class WalletPrerequisiteProjectionServiceTest { service.ensureForSubject(subject()); jdbc.update("UPDATE digital_wallet SET balance=321.00,frozen_amount=20.00 WHERE wallet_id=?", WALLET_ID); jdbc.update("UPDATE wallet_contract SET daily_used_amount=45.00 WHERE wallet_id=?", WALLET_ID); + jdbc.update("UPDATE simulated_bank_account SET balance=123.00,frozen_amount=10.00 WHERE user_id=?", SUBJECT_USER); Timestamp activationUpdatedAt = jdbc.queryForObject( "SELECT updated_at FROM central_wallet_activation WHERE id=?", Timestamp.class, ACTIVATION_ID); @@ -122,6 +129,125 @@ class WalletPrerequisiteProjectionServiceTest { BigDecimal.class, WALLET_ID)).isEqualByComparingTo("45.00"); assertThat(jdbc.queryForObject("SELECT updated_at FROM central_wallet_activation WHERE id=?", Timestamp.class, ACTIVATION_ID)).isEqualTo(activationUpdatedAt); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE user_id=?", + BigDecimal.class, SUBJECT_USER)).isEqualByComparingTo("123.00"); + assertThat(jdbc.queryForObject("SELECT frozen_amount FROM simulated_bank_account WHERE user_id=?", + BigDecimal.class, SUBJECT_USER)).isEqualByComparingTo("10.00"); + } + + @Test + void unusedLegacyShellIsRestoredInPlaceFromCompletedModuleThreeAndOnlyOnce() { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("UPDATE wallet_application SET input_bank_card_number='6216610100007654321' WHERE user_id=?", + SUBJECT_USER); + + service.ensureForSubject(subject()); + + assertThat(jdbc.queryForObject("SELECT card_number FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("6216610100007654321"); + assertThat(jdbc.queryForObject("SELECT card_last4 FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("4321"); + assertThat(jdbc.queryForObject("SELECT bank_name FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("测试银行"); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE account_id=?", + BigDecimal.class, accountId)).isEqualByComparingTo("50000.00"); + assertThat(jdbc.queryForObject("SELECT bank_account_id FROM wallet_bank_binding WHERE wallet_id=?", + String.class, WALLET_ID)).isEqualTo(accountId); + ExchangeContext context = exchangeResources.loadContext(new ExchangeActor(SUBJECT_USER, "test", SCHOOL_ID, CLASS_ID)); + assertThat(context.getBankAccountId()).isEqualTo(accountId); + assertThat(context.getBankCardNumber()).isEqualTo("6216610100007654321"); + assertThat(context.getBankCardLast4()).isEqualTo("4321"); + assertThat(context.getBankName()).isEqualTo("测试银行"); + assertThat(context.getBankAccountBalance()).isEqualByComparingTo("50000.00"); + + jdbc.update("UPDATE simulated_bank_account SET balance=49000.00 WHERE account_id=?", accountId); + service.ensureForSubject(subject()); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE account_id=?", + BigDecimal.class, accountId)).isEqualByComparingTo("49000.00"); + } + + @ParameterizedTest + @ValueSource(strings = {"balance=1", "frozen_amount=1", "bank_name='已有开户行'", "status='DISABLED'"}) + void nonShellAccountWithDifferentCardIsNeverOverwritten(String state) { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("UPDATE simulated_bank_account SET " + state + " WHERE account_id=?", accountId); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享银行账户与模块三银行卡结果不一致"); + assertThat(jdbc.queryForObject("SELECT card_number FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("6222020000000000000"); + } + + @Test + void evenCancelledExchangeHistoryPreventsShellRestoration() { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("INSERT INTO currency_exchange_order (id,request_no,user_id,wallet_id,bank_account_id,bank_code," + + "organization_id,amount,currency,request_timestamp,message_text,signing_original_text,digest," + + "status,created_at,updated_at,created_by,updated_by) VALUES " + + "('00000000-0000-0000-0000-000000000880','PROJECTION_HISTORY',?,?,?,?," + + "'ORG_EXPECTED',1,'CNY','20260904090000','TEST','TEST','TEST','CANCELLED'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test')", + SUBJECT_USER, WALLET_ID, accountId, BANK_CODE); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享银行账户与模块三银行卡结果不一致"); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE account_id=?", + BigDecimal.class, accountId)).isEqualByComparingTo("0"); + } + + @Test + void laterWalletConflictRollsBackShellRestoration() { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("UPDATE digital_wallet SET central_bank_confirmation_signature='OTHER' WHERE wallet_id=?", WALLET_ID); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享钱包与模块三最终结果不一致"); + assertThat(jdbc.queryForObject("SELECT card_number FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEqualTo("6222020000000000000"); + assertThat(jdbc.queryForObject("SELECT balance FROM simulated_bank_account WHERE account_id=?", + BigDecimal.class, accountId)).isEqualByComparingTo("0"); + } + + @Test + void incompleteModuleThreeCannotRestoreExistingShell() { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("UPDATE central_wallet_activation SET final_sent=FALSE WHERE id=?", ACTIVATION_ID); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("个人数字钱包开通"); + assertThat(jdbc.queryForObject("SELECT bank_name FROM simulated_bank_account WHERE account_id=?", + String.class, accountId)).isEmpty(); + } + + @Test + void accountBoundToAnotherWalletCannotBeRestored() { + seedCompleted(); + String accountId = seedBoundShell(); + jdbc.update("INSERT INTO digital_wallet (wallet_id,user_id,wallet_type,status,balance,frozen_amount," + + "central_bank_confirmation_signature,opened_at,created_at,updated_at) VALUES " + + "('WALLET_PROJECTION_OTHER',?,'TYPE_II','ACTIVE',0,0,'OTHER'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", OTHER_USER); + try { + jdbc.update("INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at) " + + "VALUES ('WALLET_PROJECTION_OTHER',?,'BOUND',CURRENT_TIMESTAMP)", accountId); + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享银行账户与模块三银行卡结果不一致"); + } finally { + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id='WALLET_PROJECTION_OTHER'"); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id='WALLET_PROJECTION_OTHER'"); + } + } + + private String seedBoundShell() { + service.ensureForSubject(subject()); + jdbc.update("UPDATE simulated_bank_account SET bank_name='',card_number='6222020000000000000'," + + "card_last4='0000',balance=0,frozen_amount=0 WHERE user_id=?", SUBJECT_USER); + return jdbc.queryForObject("SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); } @Test @@ -159,6 +285,8 @@ class WalletPrerequisiteProjectionServiceTest { } private void clean() { + jdbc.update("DELETE FROM exchange_debit_record WHERE exchange_id IN (SELECT id FROM currency_exchange_order WHERE user_id=?)", SUBJECT_USER); + jdbc.update("DELETE FROM currency_exchange_order WHERE user_id=?", SUBJECT_USER); jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", WALLET_ID); jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", WALLET_ID); jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", WALLET_ID);