rustjr-account-management/tests/integration/reconciliation_tests.rs
tangweijie d7f81893c5 Initial commit: 完整的 Rust 账户管理系统
- 实现账户管理改进设计文档中的所有核心功能
- 三科目余额管理 (个人余额、劳动报酬、冻结余额)
- 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed)
- 三键幂等体系 (JZTxId/BankTxId/SourceKey)
- 优先级扣款规则 (先个人后劳动)
- 在途资金管理 (可用→在途→结转/回退)
- 三账对账闭环 (总账 = 银行账 + 在途净额)
- 补偿服务域 (超时检测、重试、死信队列)
- 虚拟银行模拟器用于业务测试
- 完整的集成测试套件 (133 个测试全部通过)
- Docker 容器化部署配置
- 前端 Vue3 + TypeScript 项目结构
2026-01-05 17:56:01 +08:00

359 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 对账集成测试
use chrono::Utc;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use rustjr::domain::ledger::entity::{AccountBalance, ThreeAccountResult};
use rustjr::domain::account::AccountType;
use rustjr::infrastructure::bank_integration::mock_bank::MockBankClient;
use rustjr::infrastructure::bank_integration::BankClient;
// ==================== 测试辅助 ====================
fn create_balance(personal: Decimal, labor: Decimal, frozen: Decimal, transit: Decimal) -> AccountBalance {
AccountBalance {
id: 1,
account_id: 1001,
account_type: AccountType::Virtual,
personal_balance: personal,
labor_balance: labor,
frozen_balance: frozen,
bank_balance: personal + labor + frozen,
transit_amount: transit,
system_balance: personal + labor,
available_balance: personal + labor,
frozen_amount: frozen,
version: 1,
updated_at: Utc::now(),
}
}
/// 执行三账校验
fn verify_three_accounts(balance: &AccountBalance, bank_balance: Decimal) -> ThreeAccountResult {
let ledger_total = balance.personal_balance + balance.labor_balance + balance.frozen_balance;
let transit_net = balance.transit_amount;
// 公式:总账 + 在途 = 银行
let expected_bank = ledger_total + transit_net;
let difference = bank_balance - expected_bank;
let is_balanced = difference.abs() < dec!(0.01); // 允许 1 分钱误差
ThreeAccountResult {
bank_balance,
transit_net,
ledger_total,
is_balanced,
difference,
}
}
// ==================== 三账平衡测试 ====================
#[test]
fn test_three_account_balanced_no_transit() {
// 没有在途时的三账平衡
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
let bank = balance.bank_balance; // 8000
let result = verify_three_accounts(&balance, bank);
assert!(result.is_balanced);
assert_eq!(result.difference, dec!(0.00));
assert_eq!(result.ledger_total, dec!(8000.00));
assert_eq!(result.transit_net, dec!(0.00));
}
#[test]
fn test_three_account_balanced_with_transit() {
// 有在途时的三账平衡
// 假设:扣款 1000 进入在途,银行尚未确认
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
balance.deduct_with_priority(dec!(1000.00)).unwrap();
balance.add_transit(dec!(1000.00));
// 此时:
// 总账 = 4000 + 3000 + 0 = 7000
// 在途 = 1000
// 银行余额应该还是 8000因为银行还没确认
// 但由于我们的 deduct_with_priority 同时减少了 bank_balance
// 在实际系统中bank_balance 应该是从银行同步的
// 这里我们手动设置正确的银行余额来测试
let actual_bank = dec!(8000.00); // 银行还没扣
let result = verify_three_accounts(&balance, actual_bank);
assert!(result.is_balanced);
// 7000 + 1000 = 8000
}
#[test]
fn test_three_account_balanced_with_frozen() {
let balance = create_balance(dec!(5000.00), dec!(2000.00), dec!(1000.00), dec!(0.00));
let bank = balance.bank_balance; // 8000
let result = verify_three_accounts(&balance, bank);
assert!(result.is_balanced);
assert_eq!(result.ledger_total, dec!(8000.00)); // 5000 + 2000 + 1000
}
// ==================== 三账不平衡测试 ====================
#[test]
fn test_three_account_short() {
// 短款:银行少于预期
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
let bank = dec!(7500.00); // 银行少了 500
let result = verify_three_accounts(&balance, bank);
assert!(!result.is_balanced);
assert_eq!(result.difference, dec!(-500.00)); // 银行少 500
}
#[test]
fn test_three_account_long() {
// 长款:银行多于预期
let balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
let bank = dec!(8500.00); // 银行多了 500
let result = verify_three_accounts(&balance, bank);
assert!(!result.is_balanced);
assert_eq!(result.difference, dec!(500.00)); // 银行多 500
}
#[test]
fn test_three_account_transit_mismatch() {
// 在途不匹配
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
balance.deduct_with_priority(dec!(1000.00)).unwrap();
balance.add_transit(dec!(1000.00));
// 银行已经扣款(不应该这么快)
let bank = dec!(7000.00); // 银行已扣款
let result = verify_three_accounts(&balance, bank);
// 总账 7000 + 在途 1000 = 8000但银行只有 7000
assert!(!result.is_balanced);
assert_eq!(result.difference, dec!(-1000.00));
}
// ==================== 对账流水比对测试 ====================
#[tokio::test]
async fn test_reconciliation_with_bank_statements() {
let client = MockBankClient::new();
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
client.create_account("EXT001", "外部账户", dec!(50000.00)).unwrap();
// 模拟几笔交易
use rustjr::infrastructure::bank_integration::BankTransferRequest;
// 出账
let request = BankTransferRequest {
from_account: "PRISON001".to_string(),
to_account: "EXT001".to_string(),
to_account_name: "外部账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(5000.00),
remark: Some("提现".to_string()),
business_no: "TXN001".to_string(),
};
client.transfer(request).await.unwrap();
// 外部入账
client.simulate_external_deposit(
"PRISON001",
"FAMILY001",
"家属",
dec!(2000.00),
Some("家属充值".to_string()),
).unwrap();
// 查询银行流水
let today = Utc::now().date_naive();
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
// 验证流水
assert_eq!(statements.len(), 2);
// 找出入账
let inbound = statements.iter().filter(|s| s.direction == "in").collect::<Vec<_>>();
assert_eq!(inbound.len(), 1);
assert_eq!(inbound[0].amount, dec!(2000.00));
// 找出账
let outbound = statements.iter().filter(|s| s.direction == "out").collect::<Vec<_>>();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].amount, dec!(5000.00));
// 验证最终余额
let balance = client.query_balance("PRISON001").await.unwrap();
assert_eq!(balance.balance, dec!(97000.00)); // 100000 - 5000 + 2000
}
// ==================== 外部入账识别测试 ====================
#[tokio::test]
async fn test_external_deposit_recognition() {
let client = MockBankClient::new();
client.create_account("PRISON001", "监狱账户", dec!(100000.00)).unwrap();
// 系统不知道的外部入账
let bank_ref = client.simulate_external_deposit(
"PRISON001",
"UNKNOWN_FAMILY",
"未知家属",
dec!(3000.00),
Some("不明来源充值".to_string()),
).unwrap();
// 查询流水会发现这笔入账
let today = Utc::now().date_naive();
let statements = client.query_statements("PRISON001", today, today).await.unwrap();
let external = statements.iter()
.find(|s| s.bank_ref_no == bank_ref)
.unwrap();
assert_eq!(external.direction, "in");
assert_eq!(external.amount, dec!(3000.00));
// 在对账时,这笔交易应该标记为 "外部入账待确认"
// 需要人工或自动匹配到对应的罪犯账户
}
// ==================== 超时交易对账测试 ====================
#[test]
fn test_timeout_transaction_reconciliation() {
// 模拟超时交易场景
let mut balance = create_balance(dec!(5000.00), dec!(3000.00), dec!(0.00), dec!(0.00));
// 扣款并建立在途
balance.deduct_with_priority(dec!(1000.00)).unwrap();
balance.add_transit(dec!(1000.00));
// 此时:
// 系统认为:银行余额 7000在途 1000
// 如果银行实际已成功:银行余额 7000正确
// 如果银行实际未成功:银行余额 8000在途应回退
// 场景1银行实际已成功
let actual_bank_success = dec!(7000.00);
let result = verify_three_accounts(&balance, actual_bank_success);
// 7000 + 1000 = 8000但银行只有 7000
assert!(!result.is_balanced);
// 这说明需要结转在途
// 场景2银行实际未成功
let actual_bank_failed = dec!(8000.00);
let result = verify_three_accounts(&balance, actual_bank_failed);
// 7000 + 1000 = 8000银行也是 8000
assert!(result.is_balanced);
// 但这意味着银行没扣款,我们需要回退在途
}
// ==================== 重复交易检测测试 ====================
#[tokio::test]
async fn test_duplicate_transaction_detection() {
let client = MockBankClient::new();
client.create_account("FROM001", "转出账户", dec!(10000.00)).unwrap();
client.create_account("TO001", "转入账户", dec!(0.00)).unwrap();
use rustjr::infrastructure::bank_integration::BankTransferRequest;
let request = BankTransferRequest {
from_account: "FROM001".to_string(),
to_account: "TO001".to_string(),
to_account_name: "转入账户".to_string(),
to_bank_code: "MOCK".to_string(),
amount: dec!(1000.00),
remark: None,
business_no: "TXN_DUP_001".to_string(),
};
// 第一次转账
let response1 = client.transfer(request.clone()).await.unwrap();
assert!(response1.success);
// 用相同的 business_no 查询,应该能找到
let status = client.query_transaction_status("TXN_DUP_001").await.unwrap();
assert!(status.success);
assert_eq!(status.bank_ref_no, response1.bank_ref_no);
}
// ==================== 三账校验公式验证 ====================
#[test]
fn test_three_account_formula() {
// 验证公式:总账余额 = 银行余额 + 在途净额
// 即personal + labor + frozen = bank - transit_out + transit_in
// 简化为total = bank (当没有在途时)
struct TestCase {
personal: Decimal,
labor: Decimal,
frozen: Decimal,
transit: Decimal,
bank: Decimal,
expected_balanced: bool,
}
let cases = vec![
// 平衡:无在途
TestCase {
personal: dec!(5000.00),
labor: dec!(3000.00),
frozen: dec!(0.00),
transit: dec!(0.00),
bank: dec!(8000.00),
expected_balanced: true,
},
// 平衡:有在途(银行未确认)
TestCase {
personal: dec!(4000.00), // 扣了1000
labor: dec!(3000.00),
frozen: dec!(0.00),
transit: dec!(1000.00), // 在途1000
bank: dec!(8000.00), // 银行还没扣
expected_balanced: true,
},
// 不平衡:银行已扣但在途未清
TestCase {
personal: dec!(4000.00),
labor: dec!(3000.00),
frozen: dec!(0.00),
transit: dec!(1000.00),
bank: dec!(7000.00), // 银行已扣
expected_balanced: false,
},
// 平衡:有冻结
TestCase {
personal: dec!(4000.00),
labor: dec!(3000.00),
frozen: dec!(1000.00),
transit: dec!(0.00),
bank: dec!(8000.00),
expected_balanced: true,
},
];
for (i, case) in cases.iter().enumerate() {
let balance = create_balance(case.personal, case.labor, case.frozen, case.transit);
let result = verify_three_accounts(&balance, case.bank);
assert_eq!(
result.is_balanced, case.expected_balanced,
"Case {} failed: expected balanced={}, got balanced={}",
i, case.expected_balanced, result.is_balanced
);
}
}