diff --git a/lsfx-mock-server/services/statement_rule_samples.py b/lsfx-mock-server/services/statement_rule_samples.py index 67242902..f1e970d9 100644 --- a/lsfx-mock-server/services/statement_rule_samples.py +++ b/lsfx-mock-server/services/statement_rule_samples.py @@ -811,6 +811,117 @@ def build_salary_unused_samples(group_id: int, log_id: int, **kwargs) -> List[Di ] +def build_sudden_account_closure_samples( + group_id: int, + log_id: int, + *, + account_fact: Dict, + le_name: str = "模型测试主体", +) -> List[Dict]: + invalid_date = datetime.strptime(account_fact["invalid_date"], "%Y-%m-%d") + owner_id_card = account_fact["owner_id_card"] + account_no = account_fact["account_no"] + account_name = account_fact["account_name"] + + return [ + _build_statement( + group_id, + log_id, + trx_datetime=invalid_date - timedelta(days=30, hours=-1), + cret_no=owner_id_card, + customer_name="杭州临时往来款账户", + user_memo=f"{account_name}销户前资金回笼", + cash_type="对私转账", + cr_amount=88000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666610001", + ), + _build_statement( + group_id, + log_id, + trx_datetime=invalid_date - timedelta(days=12, hours=2), + cret_no=owner_id_card, + customer_name="杭州消费支付商户", + user_memo=f"{account_name}销户前集中支出", + cash_type="快捷支付", + dr_amount=62000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666610002", + ), + _build_statement( + group_id, + log_id, + trx_datetime=invalid_date - timedelta(days=1, hours=3), + cret_no=owner_id_card, + customer_name="浙江异常账户清理专户", + user_memo=f"{account_name}异常账户销户前转出", + cash_type="对私转账", + dr_amount=126000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666610003", + ), + ] + + +def build_dormant_account_large_activation_samples( + group_id: int, + log_id: int, + *, + account_fact: Dict, + le_name: str = "模型测试主体", +) -> List[Dict]: + effective_date = datetime.strptime(account_fact["effective_date"], "%Y-%m-%d") + activation_start = datetime(effective_date.year, effective_date.month, effective_date.day) + timedelta(days=181) + owner_id_card = account_fact["owner_id_card"] + account_no = account_fact["account_no"] + account_name = account_fact["account_name"] + + return [ + _build_statement( + group_id, + log_id, + trx_datetime=activation_start, + cret_no=owner_id_card, + customer_name="浙江存量资产回收账户", + user_memo=f"{account_name}休眠后异常账户激活入账", + cash_type="对公转账", + cr_amount=180000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666620001", + ), + _build_statement( + group_id, + log_id, + trx_datetime=activation_start + timedelta(days=9, hours=2), + cret_no=owner_id_card, + customer_name="浙江大额往来备付金专户", + user_memo=f"{account_name}休眠激活后大额转入", + cash_type="对公转账", + cr_amount=260000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666620002", + ), + _build_statement( + group_id, + log_id, + trx_datetime=activation_start + timedelta(days=18, hours=1), + cret_no=owner_id_card, + customer_name="杭州临时资金调拨账户", + user_memo=f"{account_name}休眠账户异常账户激活转出", + cash_type="对私转账", + dr_amount=120000.0, + le_name=le_name, + account_mask_no=account_no, + customer_account_mask_no="6222024666620003", + ), + ] + + LARGE_TRANSACTION_BUILDERS = { "HOUSE_OR_CAR_EXPENSE": build_house_or_car_samples, "TAX_EXPENSE": build_tax_samples, diff --git a/lsfx-mock-server/tests/test_statement_service.py b/lsfx-mock-server/tests/test_statement_service.py index 4e9973e1..788f6084 100644 --- a/lsfx-mock-server/tests/test_statement_service.py +++ b/lsfx-mock-server/tests/test_statement_service.py @@ -4,6 +4,7 @@ StatementService 主绑定注入测试 from collections import Counter, defaultdict +import services.statement_rule_samples as statement_rule_samples from services.file_service import FileService from services.statement_service import StatementService from services.statement_rule_samples import ( @@ -234,6 +235,47 @@ def test_generate_statements_should_follow_rule_hit_plan_from_file_record(): assert not any("购汇" in item["userMemo"] for item in statements) +def test_sudden_account_closure_samples_should_stay_within_30_days_before_invalid_date(): + statements = statement_rule_samples.build_sudden_account_closure_samples( + group_id=1000, + log_id=20001, + account_fact={ + "account_no": "6222000000000001", + "owner_id_card": "320101199001010030", + "account_name": "测试员工工资卡", + "status": 2, + "effective_date": "2024-01-01", + "invalid_date": "2026-03-20", + }, + le_name="测试主体", + ) + + assert statements + assert all("6222000000000001" == item["accountMaskNo"] for item in statements) + assert all("2026-02-18" <= item["trxDate"][:10] < "2026-03-20" for item in statements) + + +def test_dormant_account_large_activation_samples_should_exceed_threshold_after_6_months(): + statements = statement_rule_samples.build_dormant_account_large_activation_samples( + group_id=1000, + log_id=20001, + account_fact={ + "account_no": "6222000000000002", + "owner_id_card": "320101199001010030", + "account_name": "测试员工工资卡", + "status": 1, + "effective_date": "2025-01-01", + "invalid_date": None, + }, + le_name="测试主体", + ) + + assert statements + assert min(item["trxDate"][:10] for item in statements) >= "2025-07-01" + assert sum(item["drAmount"] + item["crAmount"] for item in statements) >= 500000 + assert max(item["drAmount"] + item["crAmount"] for item in statements) >= 100000 + + def test_generate_statements_should_stay_within_single_employee_scope_per_log_id(): """同一 logId 的流水只能落在 FileRecord 绑定的员工及亲属身份证内。""" file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())