Files
fluxent-app/packages/fluxent/tests/test_transactions.py
T
2026-06-23 14:52:00 +08:00

857 lines
28 KiB
Python

import pytest
from datetime import UTC, datetime
from decimal import Decimal
from pydantic import ValidationError
from models.ref import ChannelRef, TransactionRef
from models.transaction import (
ATMTransaction,
BaseTransaction,
DcFlag,
EcomPaymentTransaction,
FxRate,
MiscInTransaction,
MiscOutTransaction,
PaymentTransaction,
PosPaymentTransaction,
Transaction,
TransactionStatus,
TransferTransaction,
)
from ulid import ULID
@pytest.fixture
def channel_ref():
"""创建渠道引用 fixture"""
from ulid import ULID
return ChannelRef(ref_id=ULID())
@pytest.fixture
def minimal_txn(channel_ref):
"""创建最简交易 fixture"""
return BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
)
class TestBaseTransactionCreation:
"""测试基础交易创建"""
def test_minimal_transaction(self, minimal_txn):
"""最简交易创建"""
assert minimal_txn.txn_amt == Decimal("100.00")
assert minimal_txn.txn_ccy == "CNY"
assert minimal_txn.dc_flag == DcFlag.DEBIT
assert minimal_txn.cp == "商户A"
assert minimal_txn.txn_sts == TransactionStatus.COMPLETED
assert minimal_txn.version == 0
assert minimal_txn.ref_transactions == []
assert minimal_txn.deleted_at is None
assert minimal_txn.posting_amt is None
assert minimal_txn.comm_amt is None
assert minimal_txn.surcharge_amt is None
assert minimal_txn.disc_amt is None
assert minimal_txn.fx_rates is None
assert minimal_txn.acq_inst is None
assert minimal_txn.clearing_network is None
assert minimal_txn.description is None
assert minimal_txn.memo is None
assert minimal_txn.raw_description is None
assert minimal_txn.raw_data is None
assert minimal_txn.txn_scene is None
def test_full_transaction(self, channel_ref):
"""完整字段交易 - 闲鱼卖电脑场景"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("5200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="买家_小明",
posting_date=datetime.now(UTC),
posting_amt=Decimal("5148.00"),
posting_ccy="CNY",
comm_amt=Decimal("52.00"),
comm_ccy="CNY",
txn_sts=TransactionStatus.COMPLETED,
acq_inst="支付宝",
description="出售 MacBook Pro 14寸 M3",
memo="二手电脑交易",
raw_description="闲鱼订单 20260622001",
raw_data={"platform": "闲鱼", "order_id": "20260622001", "item": "MacBook Pro"},
ref_channel=channel_ref,
)
assert txn.txn_amt == Decimal("5200.00")
assert txn.txn_ccy == "CNY"
assert txn.dc_flag == DcFlag.CREDIT
assert txn.posting_amt == Decimal("5148.00")
assert txn.posting_ccy == "CNY"
assert txn.comm_amt == Decimal("52.00")
assert txn.comm_ccy == "CNY"
assert txn.txn_sts == TransactionStatus.COMPLETED
assert txn.acq_inst == "支付宝"
assert txn.description == "出售 MacBook Pro 14寸 M3"
assert txn.memo == "二手电脑交易"
assert txn.raw_description == "闲鱼订单 20260622001"
assert txn.raw_data == {"platform": "闲鱼", "order_id": "20260622001", "item": "MacBook Pro"}
def test_cp_as_channel_ref(self, channel_ref):
"""对手方为渠道引用"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("50.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp=channel_ref,
ref_channel=channel_ref,
)
assert isinstance(txn.cp, ChannelRef)
assert txn.cp.ref_type == "CHANNEL"
def test_cp_as_string(self, channel_ref):
"""对手方为字符串"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("50.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
)
assert txn.cp == "商户A"
def test_clearing_network(self, channel_ref):
"""清算网络字段"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
clearing_network="银联",
)
assert txn.clearing_network == "银联"
class TestAmtCcyPairValidation:
"""测试金额/币种对校验"""
def test_all_pairs_none(self, minimal_txn):
"""所有金额/币种对都为 None(应该通过)"""
assert minimal_txn.posting_amt is None
assert minimal_txn.comm_amt is None
assert minimal_txn.surcharge_amt is None
assert minimal_txn.disc_amt is None
def test_posting_pair_both_set(self, channel_ref):
"""入账金额/币种都有值(应该通过)"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("100.00"),
posting_ccy="CNY",
)
assert txn.posting_amt == Decimal("100.00")
assert txn.posting_ccy == "CNY"
def test_posting_amt_only_should_fail(self, channel_ref):
"""只有入账金额没有币种(应该失败)"""
with pytest.raises(ValidationError):
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("95.00"),
)
def test_posting_ccy_only_should_fail(self, channel_ref):
"""只有入账币种没有金额(应该失败)"""
with pytest.raises(ValidationError):
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_ccy="USD",
)
def test_comm_pair_both_set(self, channel_ref):
"""佣金金额/币种都有值(应该通过)"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
comm_amt=Decimal("2.00"),
comm_ccy="CNY",
)
assert txn.comm_amt == Decimal("2.00")
assert txn.comm_ccy == "CNY"
def test_comm_amt_only_should_fail(self, channel_ref):
"""只有佣金金额没有币种(应该失败)"""
with pytest.raises(ValidationError):
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
comm_amt=Decimal("2.00"),
)
def test_surcharge_pair_both_set(self, channel_ref):
"""服务费金额/币种都有值(应该通过)"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
surcharge_amt=Decimal("3.00"),
surcharge_ccy="CNY",
)
assert txn.surcharge_amt == Decimal("3.00")
assert txn.surcharge_ccy == "CNY"
def test_surcharge_amt_only_should_fail(self, channel_ref):
"""只有服务费金额没有币种(应该失败)"""
with pytest.raises(ValidationError):
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
surcharge_amt=Decimal("3.00"),
)
def test_disc_pair_both_set(self, channel_ref):
"""折扣金额/币种都有值(应该通过)"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
disc_amt=Decimal("5.00"),
disc_ccy="CNY",
)
assert txn.disc_amt == Decimal("5.00")
assert txn.disc_ccy == "CNY"
def test_disc_amt_only_should_fail(self, channel_ref):
"""只有折扣金额没有币种(应该失败)"""
with pytest.raises(ValidationError):
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
disc_amt=Decimal("5.00"),
)
def test_multiple_pairs_set(self, channel_ref):
"""多对金额/币种同时设置(应该通过)"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("1000.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("1010.00"),
posting_ccy="CNY",
comm_amt=Decimal("10.00"),
comm_ccy="CNY",
)
assert txn.posting_amt == Decimal("1010.00")
assert txn.comm_amt == Decimal("10.00")
class TestAccountingEquation:
"""测试会计恒等式校验"""
def test_credit_equation_valid(self, channel_ref):
"""CREDIT: posting_amt = txn_amt + surcharge_amt - comm_amt - disc_amt"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("5200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="买家_小明",
ref_channel=channel_ref,
posting_amt=Decimal("5148.00"),
posting_ccy="CNY",
comm_amt=Decimal("52.00"),
comm_ccy="CNY",
)
assert txn.posting_amt == Decimal("5148.00")
def test_credit_equation_with_surcharge(self, channel_ref):
"""CREDIT: 含附加费"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("1000.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="客户A",
ref_channel=channel_ref,
posting_amt=Decimal("1005.00"),
posting_ccy="CNY",
comm_amt=Decimal("10.00"),
comm_ccy="CNY",
surcharge_amt=Decimal("15.00"),
surcharge_ccy="CNY",
)
assert txn.posting_amt == Decimal("1005.00")
def test_credit_equation_invalid(self, channel_ref):
"""CREDIT: 恒等式不成立应失败"""
with pytest.raises(ValidationError) as exc_info:
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("5200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="买家_小明",
ref_channel=channel_ref,
posting_amt=Decimal("5000.00"),
posting_ccy="CNY",
comm_amt=Decimal("52.00"),
comm_ccy="CNY",
)
assert "Accounting equation mismatch" in str(exc_info.value)
def test_debit_equation_valid(self, channel_ref):
"""DEBIT: posting_amt = txn_amt + surcharge_amt + comm_amt - disc_amt"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("110.00"),
posting_ccy="CNY",
comm_amt=Decimal("10.00"),
comm_ccy="CNY",
)
assert txn.posting_amt == Decimal("110.00")
def test_debit_equation_with_disc(self, channel_ref):
"""DEBIT: 含折扣"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("105.00"),
posting_ccy="CNY",
comm_amt=Decimal("10.00"),
comm_ccy="CNY",
disc_amt=Decimal("5.00"),
disc_ccy="CNY",
)
assert txn.posting_amt == Decimal("105.00")
def test_debit_equation_invalid(self, channel_ref):
"""DEBIT: 恒等式不成立应失败"""
with pytest.raises(ValidationError) as exc_info:
BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("100.00"),
posting_ccy="CNY",
comm_amt=Decimal("10.00"),
comm_ccy="CNY",
)
assert "Accounting equation mismatch" in str(exc_info.value)
def test_different_currency_skip_validation(self, channel_ref):
"""不同币种跳过校验"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("5200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="买家_小明",
ref_channel=channel_ref,
posting_amt=Decimal("700.00"),
posting_ccy="USD",
)
assert txn.posting_amt == Decimal("700.00")
def test_no_posting_amt_skip_validation(self, channel_ref):
"""无 posting_amt 跳过校验"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("5200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="买家_小明",
ref_channel=channel_ref,
)
assert txn.posting_amt is None
class TestFxRate:
"""测试汇率快照"""
def test_fx_rate_creation(self):
"""创建汇率快照"""
fx = FxRate(from_ccy="USD", to_ccy="CNY", rate=Decimal("6.75"))
assert fx.from_ccy == "USD"
assert fx.to_ccy == "CNY"
assert fx.rate == Decimal("6.75")
def test_invalid_currency_should_fail(self):
"""无效币种应该失败"""
with pytest.raises(ValidationError):
FxRate(from_ccy="INVALID", to_ccy="CNY", rate=Decimal("6.75"))
def test_transaction_with_fx_rates(self, channel_ref):
"""交易带多个汇率快照"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="USD",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
fx_rates=[
FxRate(from_ccy="USD", to_ccy="CNY", rate=Decimal("6.75")),
FxRate(from_ccy="USD", to_ccy="HKD", rate=Decimal("7.82")),
],
)
assert len(txn.fx_rates) == 2
assert txn.fx_rates[0].from_ccy == "USD"
assert txn.fx_rates[0].to_ccy == "CNY"
assert txn.fx_rates[1].from_ccy == "USD"
assert txn.fx_rates[1].to_ccy == "HKD"
class TestTransactionStatus:
"""测试交易状态"""
def test_default_status(self, minimal_txn):
"""默认状态为 COMPLETED"""
assert minimal_txn.txn_sts == TransactionStatus.COMPLETED
def test_set_pending(self, channel_ref):
"""设置为 PENDING"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
txn_sts=TransactionStatus.PENDING,
)
assert txn.txn_sts == TransactionStatus.PENDING
def test_set_failed(self, channel_ref):
"""设置为 FAILED"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
txn_sts=TransactionStatus.FAILED,
)
assert txn.txn_sts == TransactionStatus.FAILED
def test_set_refunded(self, channel_ref):
"""设置为 REFUNDED"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
txn_sts=TransactionStatus.REFUNDED,
)
assert txn.txn_sts == TransactionStatus.REFUNDED
class TestDcFlag:
"""测试借贷方向"""
def test_debit(self, channel_ref):
"""借方"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
)
assert txn.dc_flag == DcFlag.DEBIT
def test_credit(self, channel_ref):
"""贷方"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="商户A",
ref_channel=channel_ref,
)
assert txn.dc_flag == DcFlag.CREDIT
class TestTransactionRef:
"""测试交易引用"""
def test_ref_transactions(self, channel_ref):
"""关联交易引用"""
ref = TransactionRef(ref_id=ULID())
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
ref_transactions=[ref],
)
assert len(txn.ref_transactions) == 1
assert txn.ref_transactions[0].ref_type == "TRANSACTION"
class TestSoftDelete:
def test_not_deleted(self, minimal_txn):
"""未删除状态"""
assert minimal_txn.deleted_at is None
def test_set_deleted_at(self, minimal_txn):
"""设置删除时间"""
deleted_at = datetime.now(UTC)
minimal_txn.deleted_at = deleted_at
assert minimal_txn.deleted_at == deleted_at
class TestVersion:
"""测试乐观锁"""
def test_default_version(self, minimal_txn):
"""默认版本为 0"""
assert minimal_txn.version == 0
def test_set_version(self, channel_ref):
"""设置版本号"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
version=5,
)
assert txn.version == 5
class TestOptionalFields:
"""测试可选字段"""
def test_empty_description_allowed(self, channel_ref):
"""空字符串描述允许"""
txn = BaseTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
description="",
)
assert txn.description == ""
def test_all_optional_fields_none(self, minimal_txn):
"""所有可选字段都为 None"""
assert minimal_txn.description is None
assert minimal_txn.memo is None
assert minimal_txn.raw_description is None
assert minimal_txn.raw_data is None
assert minimal_txn.acq_inst is None
assert minimal_txn.clearing_network is None
class TestMiscInTransaction:
"""测试其他收入交易"""
def test_create(self, channel_ref):
"""创建其他收入交易"""
txn = MiscInTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("500.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="红包",
ref_channel=channel_ref,
posting_amt=Decimal("500.00"),
posting_ccy="CNY",
)
assert txn.txn_scene == "MISC_IN"
def test_txn_scene_fixed(self, channel_ref):
"""txn_scene 固定为 MISC_IN"""
txn = MiscInTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.CREDIT,
cp="退款",
ref_channel=channel_ref,
posting_amt=Decimal("100.00"),
posting_ccy="CNY",
)
assert txn.txn_scene == "MISC_IN"
class TestMiscOutTransaction:
"""测试其他支出交易"""
def test_create(self, channel_ref):
"""创建其他支出交易"""
txn = MiscOutTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("200.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="捐赠",
ref_channel=channel_ref,
posting_amt=Decimal("200.00"),
posting_ccy="CNY",
)
assert txn.txn_scene == "MISC_OUT"
class TestPaymentTransaction:
"""测试支付交易"""
def test_create(self, channel_ref):
"""创建支付交易"""
txn = PaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户A",
ref_channel=channel_ref,
posting_amt=Decimal("100.00"),
posting_ccy="CNY",
merchant_name="星巴克",
)
assert txn.txn_scene == "PAYMENT"
assert txn.merchant_name == "星巴克"
def test_merchant_name_optional(self, channel_ref):
"""商户名称可选"""
txn = PaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("50.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户B",
ref_channel=channel_ref,
posting_amt=Decimal("50.00"),
posting_ccy="CNY",
)
assert txn.merchant_name is None
class TestEcomPaymentTransaction:
"""测试电子商务支付交易"""
def test_create(self, channel_ref):
"""创建电商支付交易"""
txn = EcomPaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("299.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="淘宝商家",
ref_channel=channel_ref,
posting_amt=Decimal("299.00"),
posting_ccy="CNY",
merchant_name="淘宝店铺",
order_id="TB20260622001",
)
assert txn.txn_scene == "ECOM_PAYMENT"
assert txn.order_id == "TB20260622001"
assert txn.merchant_name == "淘宝店铺"
def test_order_id_optional(self, channel_ref):
"""订单ID可选"""
txn = EcomPaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("100.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="京东",
ref_channel=channel_ref,
posting_amt=Decimal("100.00"),
posting_ccy="CNY",
)
assert txn.order_id is None
class TestPosPaymentTransaction:
"""测试POS支付交易"""
def test_create_with_geo(self, channel_ref):
"""创建POS支付交易(带地理信息)"""
txn = PosPaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("50.00"),
txn_ccy="HKD",
dc_flag=DcFlag.DEBIT,
cp="7-11",
ref_channel=channel_ref,
posting_amt=Decimal("50.00"),
posting_ccy="HKD",
geo=("HK"),
)
assert txn.txn_scene == "POS_PAYMENT"
assert txn.geo == ("HK",)
def test_geo_country_only(self, channel_ref):
"""地理信息仅包含国家"""
txn = PosPaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("30.00"),
txn_ccy="HKD",
dc_flag=DcFlag.DEBIT,
cp="7-11",
ref_channel=channel_ref,
posting_amt=Decimal("30.00"),
posting_ccy="HKD",
geo=("HK",),
)
assert txn.geo == ("HK",)
def test_geo_optional(self, channel_ref):
"""地理信息可选"""
txn = PosPaymentTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("20.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="商户",
ref_channel=channel_ref,
posting_amt=Decimal("20.00"),
posting_ccy="CNY",
)
assert txn.geo is None
class TestATMTransaction:
"""测试ATM交易"""
def test_create_with_geo(self, channel_ref):
"""创建ATM交易(带地理信息)"""
txn = ATMTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("2000.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="ATM",
ref_channel=channel_ref,
posting_amt=Decimal("2000.00"),
posting_ccy="CNY",
geo=("CN", "SH"),
)
assert txn.txn_scene == "ATM"
assert txn.geo == ("CN", "SH")
def test_geo_optional(self, channel_ref):
"""地理信息可选"""
txn = ATMTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("500.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="ATM",
ref_channel=channel_ref,
posting_amt=Decimal("500.00"),
posting_ccy="CNY",
)
assert txn.geo is None
class TestTransferTransaction:
"""测试转账交易"""
def test_create(self, channel_ref):
"""创建转账交易"""
txn = TransferTransaction(
txn_date=datetime.now(UTC),
txn_amt=Decimal("10000.00"),
txn_ccy="CNY",
dc_flag=DcFlag.DEBIT,
cp="朋友A",
ref_channel=channel_ref,
posting_amt=Decimal("10000.00"),
posting_ccy="CNY",
)
assert txn.txn_scene == "TRANSFER"
class TestDiscriminatedUnion:
"""测试 Discriminated Union"""
def test_all_scenes_present(self):
"""所有交易场景都已定义"""
scenes = {"MISC_IN", "MISC_OUT", "TRANSFER", "PAYMENT", "ECOM_PAYMENT", "POS_PAYMENT", "ATM"}
# 验证所有场景都有对应的模型
assert scenes == {
MiscInTransaction.model_fields["txn_scene"].default,
MiscOutTransaction.model_fields["txn_scene"].default,
TransferTransaction.model_fields["txn_scene"].default,
PaymentTransaction.model_fields["txn_scene"].default,
EcomPaymentTransaction.model_fields["txn_scene"].default,
PosPaymentTransaction.model_fields["txn_scene"].default,
ATMTransaction.model_fields["txn_scene"].default,
}