fix(exchange): restore unused shared bank account from wallet opening

chore/issuance-comments-zh
chenyuan 1 month ago
parent 74a649ca22
commit 4c1dc15d66

@ -169,7 +169,8 @@ public class WalletPrerequisiteProjectionService {
(source.userId + "|" + institution.bankCode).getBytes(StandardCharsets.UTF_8));
List<BankAccount> existing = bankAccounts(source.userId, institution.bankCode);
if (!existing.isEmpty()) {
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;
}
try {
@ -182,7 +183,8 @@ public class WalletPrerequisiteProjectionService {
} 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;
@ -291,10 +293,31 @@ public class WalletPrerequisiteProjectionService {
}
private List<BankAccount> 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) {
@ -434,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;
}
}

@ -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);

Loading…
Cancel
Save