774 lines
27 KiB
Python
774 lines
27 KiB
Python
"""
|
|
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 (
|
|
DEFAULT_LARGE_TRANSACTION_THRESHOLDS,
|
|
build_fixed_counterparty_transfer_samples,
|
|
build_large_transaction_seed_statements,
|
|
build_low_income_relative_large_transaction_samples,
|
|
build_monthly_fixed_income_samples,
|
|
build_seed_statements_for_rule_plan,
|
|
build_special_amount_transaction_samples,
|
|
build_suspicious_income_keyword_samples,
|
|
)
|
|
|
|
|
|
class FakeStaffIdentityRepository:
|
|
def select_random_staff_with_families(self):
|
|
return {
|
|
"staff_name": "数据库员工",
|
|
"staff_id_card": "320101199001010030",
|
|
"family_id_cards": ["320101199201010051", "320101199301010052"],
|
|
}
|
|
|
|
|
|
class FakeAbnormalAccountBaselineService:
|
|
def apply(self, staff_id_card, abnormal_accounts):
|
|
return None
|
|
|
|
|
|
def test_generate_statements_should_include_seeded_samples_before_noise_when_rule_plan_exists():
|
|
"""存在规则命中计划时,生成流水必须先混入被选中的命中样本。"""
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
service = StatementService(file_service=file_service)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_seeded_samples",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.large_transaction_hit_rules = ["HOUSE_OR_CAR_EXPENSE"]
|
|
record.phase1_hit_rules = []
|
|
|
|
statements = service._generate_statements(group_id=1000, log_id=log_id, count=30)
|
|
|
|
assert len(statements) >= 30
|
|
assert any(item["userMemo"] == "购买房产首付款" for item in statements)
|
|
|
|
|
|
def test_build_seed_statements_for_rule_plan_should_only_include_requested_phase1_rules():
|
|
plan = {
|
|
"large_transaction_hit_rules": [],
|
|
"phase1_hit_rules": ["GAMBLING_SENSITIVE_KEYWORD", "FOREX_BUY_AMT"],
|
|
}
|
|
|
|
statements = build_seed_statements_for_rule_plan(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
rule_plan=plan,
|
|
)
|
|
|
|
assert any("游戏" in item["userMemo"] for item in statements)
|
|
assert any("购汇" in item["userMemo"] for item in statements)
|
|
assert not any("证券" in item["userMemo"] for item in statements)
|
|
|
|
|
|
def test_build_seed_statements_for_rule_plan_should_generate_withdraw_cnt_samples():
|
|
plan = {
|
|
"large_transaction_hit_rules": [],
|
|
"phase1_hit_rules": ["WITHDRAW_CNT"],
|
|
}
|
|
|
|
statements = build_seed_statements_for_rule_plan(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
rule_plan=plan,
|
|
)
|
|
|
|
assert len(
|
|
[
|
|
item
|
|
for item in statements
|
|
if "微信提现" in item["userMemo"] or "支付宝提现" in item["userMemo"]
|
|
]
|
|
) >= 4
|
|
|
|
|
|
def test_build_seed_statements_for_rule_plan_should_only_include_requested_phase2_rules():
|
|
plan = {
|
|
"large_transaction_hit_rules": [],
|
|
"phase1_hit_rules": [],
|
|
"phase2_statement_hit_rules": [
|
|
"MULTI_PARTY_GAMBLING_TRANSFER",
|
|
"SALARY_QUICK_TRANSFER",
|
|
],
|
|
"phase2_baseline_hit_rules": [],
|
|
}
|
|
|
|
statements = build_seed_statements_for_rule_plan(
|
|
group_id=1000,
|
|
log_id=30001,
|
|
rule_plan=plan,
|
|
)
|
|
|
|
assert any(item["userMemo"] == "工资入账" for item in statements)
|
|
assert any(item["customerName"] == "欢乐游戏科技有限公司" for item in statements)
|
|
assert not any(item["userMemo"] == "季度稳定兼职收入" for item in statements)
|
|
|
|
|
|
def test_salary_quick_transfer_and_salary_unused_should_use_different_identity_groups():
|
|
plan = {
|
|
"large_transaction_hit_rules": [],
|
|
"phase1_hit_rules": [],
|
|
"phase2_statement_hit_rules": [
|
|
"SALARY_QUICK_TRANSFER",
|
|
"SALARY_UNUSED",
|
|
],
|
|
"phase2_baseline_hit_rules": [],
|
|
}
|
|
|
|
statements = build_seed_statements_for_rule_plan(
|
|
group_id=1000,
|
|
log_id=30001,
|
|
rule_plan=plan,
|
|
)
|
|
|
|
salary_id_cards = {
|
|
item["cretNo"]
|
|
for item in statements
|
|
if item["userMemo"] == "工资入账"
|
|
}
|
|
|
|
assert len(salary_id_cards) >= 2
|
|
|
|
|
|
def test_large_transaction_seed_should_cover_all_eight_rules():
|
|
"""大额交易样本生成器必须覆盖 8 条已实现规则的关键口径。"""
|
|
statements = build_large_transaction_seed_statements(group_id=1000, log_id=20001)
|
|
|
|
assert any(
|
|
item["userMemo"] == "购买房产首付款" and item["drAmount"] > 0
|
|
for item in statements
|
|
)
|
|
assert any(
|
|
"税" in item["userMemo"] and item["drAmount"] > 0
|
|
for item in statements
|
|
)
|
|
assert any(
|
|
item["crAmount"] > DEFAULT_LARGE_TRANSACTION_THRESHOLDS["SINGLE_TRANSACTION_AMOUNT"]
|
|
for item in statements
|
|
)
|
|
assert sum(
|
|
1
|
|
for item in statements
|
|
if item["customerName"] == "浙江远望贸易有限公司" and item["crAmount"] > 0
|
|
) >= 3
|
|
assert sum(
|
|
1
|
|
for item in statements
|
|
if item["cashType"] == "现金存款"
|
|
and item["crAmount"] > DEFAULT_LARGE_TRANSACTION_THRESHOLDS["LARGE_CASH_DEPOSIT"]
|
|
) >= 1
|
|
assert any(
|
|
item["userMemo"] == "手机银行转账"
|
|
and item["drAmount"] > DEFAULT_LARGE_TRANSACTION_THRESHOLDS["FREQUENT_TRANSFER"]
|
|
for item in statements
|
|
)
|
|
|
|
same_day_cash_deposits = [
|
|
item for item in statements
|
|
if item["cretNo"] == "330101198801010011"
|
|
and item["trxDate"].startswith("2026-03-10")
|
|
and item["crAmount"] > DEFAULT_LARGE_TRANSACTION_THRESHOLDS["LARGE_CASH_DEPOSIT"]
|
|
]
|
|
assert len(same_day_cash_deposits) >= (
|
|
DEFAULT_LARGE_TRANSACTION_THRESHOLDS["FREQUENT_CASH_DEPOSIT"] + 1
|
|
)
|
|
|
|
|
|
def test_large_transaction_seed_income_should_avoid_salary_exclusion():
|
|
"""收入样本不得误带工资代发关键词,否则会被后端过滤。"""
|
|
statements = build_large_transaction_seed_statements(group_id=1000, log_id=20001)
|
|
|
|
income_samples = [item for item in statements if item["crAmount"] > 0]
|
|
assert income_samples
|
|
assert all(item["customerName"] != "浙江兰溪农村商业银行股份有限公司" for item in income_samples)
|
|
assert all(
|
|
keyword not in item["userMemo"]
|
|
for item in income_samples
|
|
for keyword in ("代发", "工资", "奖金", "薪酬", "薪金")
|
|
)
|
|
|
|
|
|
def test_generate_statements_should_fill_noise_up_to_requested_count():
|
|
"""样本不足请求总数时,服务层需要自动补齐噪声流水。"""
|
|
service = StatementService()
|
|
|
|
statements = service._generate_statements(group_id=1000, log_id=20001, count=80)
|
|
|
|
assert len(statements) == 80
|
|
|
|
|
|
def test_generate_statements_should_follow_rule_hit_plan_from_file_record():
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_rule_plan",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.large_transaction_hit_rules = ["HOUSE_OR_CAR_EXPENSE"]
|
|
record.phase1_hit_rules = ["GAMBLING_SENSITIVE_KEYWORD"]
|
|
|
|
statements = statement_service._generate_statements(group_id=1001, log_id=log_id, count=200)
|
|
|
|
assert any("房产首付款" in item["userMemo"] for item in statements)
|
|
assert any("游戏" in item["userMemo"] for item in statements)
|
|
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_follow_abnormal_account_rule_plan_from_file_record():
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_abnormal_rule_plan",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.abnormal_account_hit_rules = ["SUDDEN_ACCOUNT_CLOSURE"]
|
|
record.abnormal_accounts = [
|
|
{
|
|
"account_no": "6222000000000001",
|
|
"owner_id_card": record.staff_id_card,
|
|
"account_name": "测试员工工资卡",
|
|
"status": 2,
|
|
"effective_date": "2024-01-01",
|
|
"invalid_date": "2026-03-20",
|
|
}
|
|
]
|
|
|
|
statements = statement_service._generate_statements(group_id=1001, log_id=log_id, count=80)
|
|
|
|
assert any(item["accountMaskNo"] == "6222000000000001" for item in statements)
|
|
assert any("销户" in item["userMemo"] or "异常账户" in item["userMemo"] for item in statements)
|
|
|
|
|
|
def test_get_bank_statement_should_preserve_abnormal_account_mask_no():
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_abnormal_api",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.abnormal_account_hit_rules = [
|
|
"SUDDEN_ACCOUNT_CLOSURE",
|
|
"DORMANT_ACCOUNT_LARGE_ACTIVATION",
|
|
]
|
|
record.abnormal_accounts = [
|
|
{
|
|
"account_no": "6222000000000001",
|
|
"owner_id_card": record.staff_id_card,
|
|
"account_name": "测试员工工资卡",
|
|
"status": 2,
|
|
"effective_date": "2024-01-01",
|
|
"invalid_date": "2026-03-20",
|
|
"rule_code": "SUDDEN_ACCOUNT_CLOSURE",
|
|
},
|
|
{
|
|
"account_no": "6222000000000002",
|
|
"owner_id_card": record.staff_id_card,
|
|
"account_name": "测试员工工资卡",
|
|
"status": 1,
|
|
"effective_date": "2025-01-01",
|
|
"invalid_date": None,
|
|
"rule_code": "DORMANT_ACCOUNT_LARGE_ACTIVATION",
|
|
},
|
|
]
|
|
|
|
response = statement_service.get_bank_statement(
|
|
{
|
|
"groupId": 1001,
|
|
"logId": log_id,
|
|
"pageNow": 1,
|
|
"pageSize": 500,
|
|
}
|
|
)
|
|
statements = response["data"]["bankStatementList"]
|
|
abnormal_statements = [
|
|
item for item in statements if "销户" in item["userMemo"] or "激活" in item["userMemo"]
|
|
]
|
|
|
|
assert abnormal_statements
|
|
assert any(item["accountMaskNo"] == "6222000000000001" for item in abnormal_statements)
|
|
assert any(item["accountMaskNo"] == "6222000000000002" for item in abnormal_statements)
|
|
|
|
|
|
def test_get_bank_statement_should_only_use_abnormal_account_numbers_from_file_record():
|
|
file_service = FileService(
|
|
staff_identity_repository=FakeStaffIdentityRepository(),
|
|
abnormal_account_baseline_service=FakeAbnormalAccountBaselineService(),
|
|
)
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_abnormal_statement_consistency",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.abnormal_account_hit_rules = ["SUDDEN_ACCOUNT_CLOSURE"]
|
|
record.abnormal_accounts = [
|
|
{
|
|
"account_no": "6222000000000099",
|
|
"owner_id_card": record.staff_id_card,
|
|
"account_name": "测试员工工资卡",
|
|
"status": 2,
|
|
"effective_date": "2024-01-01",
|
|
"invalid_date": "2026-03-20",
|
|
"rule_code": "SUDDEN_ACCOUNT_CLOSURE",
|
|
}
|
|
]
|
|
|
|
result = statement_service.get_bank_statement(
|
|
{"groupId": 1001, "logId": log_id, "pageNow": 1, "pageSize": 500}
|
|
)
|
|
abnormal_numbers = {
|
|
item["accountMaskNo"]
|
|
for item in result["data"]["bankStatementList"]
|
|
if "销户" in item["userMemo"] or "异常账户" in item["userMemo"]
|
|
}
|
|
|
|
assert abnormal_numbers == {"6222000000000099"}
|
|
|
|
|
|
def test_generate_statements_should_stay_within_single_employee_scope_per_log_id():
|
|
"""同一 logId 的流水只能落在 FileRecord 绑定的员工及亲属身份证内。"""
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
service = StatementService(file_service=file_service)
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_scope",
|
|
"dataChannelCode": "test",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
allowed_id_cards = {record.staff_id_card, *record.family_id_cards}
|
|
|
|
statements = service._generate_statements(group_id=1000, log_id=log_id, count=1600)
|
|
|
|
assert {item["cretNo"] for item in statements}.issubset(allowed_id_cards)
|
|
|
|
|
|
def test_get_bank_statement_should_include_counterparty_identity_fields():
|
|
service = StatementService()
|
|
|
|
response = service.get_bank_statement(
|
|
{
|
|
"groupId": 1000,
|
|
"logId": 20001,
|
|
"pageNow": 1,
|
|
"pageSize": 5,
|
|
}
|
|
)
|
|
|
|
statements = response["data"]["bankStatementList"]
|
|
|
|
assert statements
|
|
assert all("customerCertNo" in item for item in statements)
|
|
assert all("customerSocialCreditCode" in item for item in statements)
|
|
|
|
|
|
def test_all_mode_monthly_fixed_income_log_should_keep_monthly_income_stable(monkeypatch):
|
|
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
service = StatementService(file_service=file_service)
|
|
|
|
monkeypatch.setattr(
|
|
file_service,
|
|
"_generate_primary_binding",
|
|
lambda: ("全命中主体", "6222000012345678"),
|
|
)
|
|
|
|
request = {
|
|
"groupId": 1001,
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
|
|
first_log_id = file_service.fetch_inner_flow(
|
|
{**request, "customerNo": "customer_monthly_income_1"}
|
|
)["data"][0]
|
|
file_service.fetch_inner_flow(
|
|
{**request, "customerNo": "customer_monthly_income_2"}
|
|
)
|
|
record = file_service.file_records[first_log_id]
|
|
|
|
statements = service._generate_statements(group_id=1001, log_id=first_log_id, count=200)
|
|
|
|
monthly_amounts = defaultdict(float)
|
|
for item in statements:
|
|
if item["cretNo"] != record.staff_id_card:
|
|
continue
|
|
if item["crAmount"] <= 0:
|
|
continue
|
|
if not item["customerName"]:
|
|
continue
|
|
if item["leName"] == item["customerName"]:
|
|
continue
|
|
if item["customerName"] == "浙江兰溪农村商业银行股份有限公司" and (
|
|
any(keyword in item["userMemo"] for keyword in ("代发", "工资", "奖金", "薪酬", "薪金", "补贴", "薪", "年终奖", "年金", "加班费", "劳务费", "劳务外包", "提成", "劳务派遣", "绩效", "酬劳", "PAYROLL", "SALA", "CPF", "directors", "批量代付"))
|
|
or any(keyword in item["cashType"] for keyword in ("代发", "工资", "劳务费"))
|
|
):
|
|
continue
|
|
monthly_amounts[item["trxDate"][:7]] += item["crAmount"]
|
|
|
|
assert monthly_amounts
|
|
assert len(monthly_amounts) == 6
|
|
assert set(monthly_amounts.values()) == {7200.0}
|
|
|
|
|
|
def test_generate_statements_should_only_use_recognizable_identity_cards():
|
|
"""命中样本和随机噪声都只能使用现库可识别的身份证号。"""
|
|
service = StatementService()
|
|
|
|
statements = service._generate_statements(group_id=1000, log_id=20005, count=1600)
|
|
|
|
assert {item["cretNo"] for item in statements}.issubset(
|
|
{
|
|
"330101198801010011",
|
|
"330101199001010022",
|
|
"330101198802020033",
|
|
"330101199202020044",
|
|
}
|
|
)
|
|
|
|
|
|
def test_get_bank_statement_should_keep_same_cached_result_for_same_log_id():
|
|
"""同一 logId 首次生成后应复用缓存,避免分页结果漂移。"""
|
|
service = StatementService()
|
|
|
|
page1 = service.get_bank_statement(
|
|
{"groupId": 1000, "logId": 30001, "pageNow": 1, "pageSize": 20}
|
|
)
|
|
page2 = service.get_bank_statement(
|
|
{"groupId": 1000, "logId": 30001, "pageNow": 1, "pageSize": 20}
|
|
)
|
|
|
|
assert page1["data"]["bankStatementList"] == page2["data"]["bankStatementList"]
|
|
|
|
|
|
def test_get_bank_statement_should_use_fixed_total_count_200():
|
|
"""兰溪本地流水列表首次生成后,总条数应固定为 200。"""
|
|
service = StatementService()
|
|
|
|
response = service.get_bank_statement(
|
|
{"groupId": 1000, "logId": 30002, "pageNow": 1, "pageSize": 500}
|
|
)
|
|
|
|
assert response["data"]["totalCount"] == 200
|
|
assert len(response["data"]["bankStatementList"]) == 200
|
|
|
|
|
|
def test_get_bank_statement_uses_primary_binding_from_file_service(monkeypatch):
|
|
"""同一 logId 的流水记录必须复用 FileService 中的主体与账号绑定。"""
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
monkeypatch.setattr(
|
|
file_service,
|
|
"_generate_primary_binding",
|
|
lambda: ("绑定主体", "6222000011112222"),
|
|
)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_001",
|
|
"dataChannelCode": "test",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
|
|
statement_response = statement_service.get_bank_statement(
|
|
{
|
|
"groupId": 1001,
|
|
"logId": log_id,
|
|
"pageNow": 1,
|
|
"pageSize": 5,
|
|
}
|
|
)
|
|
|
|
statements = statement_response["data"]["bankStatementList"]
|
|
assert statements
|
|
assert all(item["leName"] == record.primary_enterprise_name for item in statements)
|
|
assert all(item["accountMaskNo"] == record.primary_account_no for item in statements)
|
|
|
|
|
|
def test_get_bank_statement_contains_large_transaction_hit_samples(monkeypatch):
|
|
"""流水 Mock 首次生成时必须稳定包含可命中大额交易规则的样本簇。"""
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
monkeypatch.setattr(
|
|
file_service,
|
|
"_generate_primary_binding",
|
|
lambda: ("命中主体", "6222000099998888"),
|
|
)
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_large_transaction",
|
|
"dataChannelCode": "test",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.large_transaction_hit_rules = [
|
|
"HOUSE_OR_CAR_EXPENSE",
|
|
"TAX_EXPENSE",
|
|
"CUMULATIVE_INCOME",
|
|
"FREQUENT_CASH_DEPOSIT",
|
|
"LARGE_TRANSFER",
|
|
]
|
|
record.phase1_hit_rules = []
|
|
staff_id_card = record.staff_id_card
|
|
family_id_card = record.family_id_cards[0]
|
|
|
|
statement_response = statement_service.get_bank_statement(
|
|
{
|
|
"groupId": 1001,
|
|
"logId": log_id,
|
|
"pageNow": 1,
|
|
"pageSize": 2000,
|
|
}
|
|
)
|
|
statements = statement_response["data"]["bankStatementList"]
|
|
|
|
assert statements
|
|
assert any(
|
|
item["cretNo"] in {staff_id_card, family_id_card}
|
|
for item in statements
|
|
)
|
|
assert any("房产首付款" in item["userMemo"] for item in statements)
|
|
assert any("税" in item["userMemo"] or "税务" in item["customerName"] for item in statements)
|
|
|
|
income_amounts = defaultdict(float)
|
|
cash_deposit_daily_counter = Counter()
|
|
has_large_transfer = False
|
|
|
|
for item in statements:
|
|
if (
|
|
item["cretNo"] == staff_id_card
|
|
and item["customerName"] == "浙江远望贸易有限公司"
|
|
and item["crAmount"] > 0
|
|
):
|
|
income_amounts[(item["cretNo"], item["customerName"])] += item["crAmount"]
|
|
|
|
if item["crAmount"] > 2000001 and "现金" in item["cashType"]:
|
|
cash_deposit_daily_counter[(item["cretNo"], item["trxDate"][:10])] += 1
|
|
|
|
if item["drAmount"] > 100001 and item["userMemo"] == "手机银行转账":
|
|
has_large_transfer = True
|
|
|
|
assert any(amount > 50000001 for amount in income_amounts.values())
|
|
assert any(count >= 6 for count in cash_deposit_daily_counter.values())
|
|
assert has_large_transfer
|
|
|
|
|
|
def test_special_amount_transaction_samples_should_use_sql_supported_amounts():
|
|
statements = build_special_amount_transaction_samples(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
)
|
|
|
|
assert statements
|
|
assert {item["transAmount"] for item in statements}.issubset({520.0, 1314.0})
|
|
|
|
|
|
def test_monthly_fixed_income_samples_should_cover_at_least_six_months():
|
|
statements = build_monthly_fixed_income_samples(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
)
|
|
|
|
income_months = {item["trxDate"][:7] for item in statements}
|
|
|
|
assert len(income_months) >= 6
|
|
|
|
|
|
def test_suspicious_income_keyword_samples_should_hit_sql_keyword_set():
|
|
statements = build_suspicious_income_keyword_samples(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
)
|
|
|
|
assert statements
|
|
assert any(
|
|
any(keyword in item["userMemo"] for keyword in ("工资", "分红", "红利", "奖金", "劳务费", "绩效"))
|
|
or any(keyword in item["cashType"] for keyword in ("工资", "劳务费", "代发"))
|
|
for item in statements
|
|
)
|
|
|
|
|
|
def test_fixed_counterparty_transfer_samples_should_use_staff_identity():
|
|
statements = build_fixed_counterparty_transfer_samples(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
staff_id_card="320101199001010030",
|
|
family_id_cards=["320101199201010051", "320101199301010052"],
|
|
)
|
|
|
|
assert statements
|
|
assert {item["cretNo"] for item in statements} == {"320101199001010030"}
|
|
|
|
|
|
def test_low_income_relative_large_transaction_samples_should_exceed_total_threshold():
|
|
statements = build_low_income_relative_large_transaction_samples(
|
|
group_id=1000,
|
|
log_id=20001,
|
|
staff_id_card="320101199001010030",
|
|
family_id_cards=["320101199201010051", "320101199301010052"],
|
|
)
|
|
|
|
assert statements
|
|
assert len({item["cretNo"] for item in statements}) == 1
|
|
assert sum(item["crAmount"] + item["drAmount"] for item in statements) > 100000
|
|
|
|
|
|
def test_generate_statements_should_keep_all_mode_noise_as_safe_debits(monkeypatch):
|
|
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
|
statement_service = StatementService(file_service=file_service)
|
|
|
|
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
|
|
|
|
response = file_service.fetch_inner_flow(
|
|
{
|
|
"groupId": 1001,
|
|
"customerNo": "customer_safe_noise",
|
|
"dataChannelCode": "test_code",
|
|
"requestDateId": 20240101,
|
|
"dataStartDateId": 20240101,
|
|
"dataEndDateId": 20240131,
|
|
"uploadUserId": 902001,
|
|
}
|
|
)
|
|
log_id = response["data"][0]
|
|
record = file_service.file_records[log_id]
|
|
record.large_transaction_hit_rules = []
|
|
record.phase1_hit_rules = []
|
|
record.phase2_statement_hit_rules = [
|
|
"MONTHLY_FIXED_INCOME",
|
|
"FIXED_COUNTERPARTY_TRANSFER",
|
|
]
|
|
record.abnormal_account_hit_rules = []
|
|
record.abnormal_accounts = []
|
|
|
|
statements = statement_service._generate_statements(group_id=1001, log_id=log_id, count=30)
|
|
noise_statements = [
|
|
item
|
|
for item in statements
|
|
if item["userMemo"] not in {"月度稳定兼职收入", "季度稳定兼职收入"}
|
|
]
|
|
|
|
assert noise_statements
|
|
assert all(item["drAmount"] > 0 and item["crAmount"] == 0 for item in noise_statements)
|