✅ test: add tests
This commit is contained in:
@@ -0,0 +1,857 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user