Compare commits
3 Commits
7ba6a32015
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
ac5a3c6dd7
|
|||
|
5a7dd536b8
|
|||
|
b46265cade
|
@@ -0,0 +1,37 @@
|
||||
# Fluxent 记账逻辑
|
||||
|
||||
## dc_flag 语义
|
||||
|
||||
`dc_flag` 表示**资金流向**(从用户角度),不反转:
|
||||
|
||||
- `DEBIT` = 资金流出(支出)
|
||||
- `CREDIT` = 资金流入(收入)
|
||||
|
||||
## 账户类型与余额变化
|
||||
|
||||
`account_type` 决定余额如何变化:
|
||||
|
||||
| 账户类型 | DEBIT(支出) | CREDIT(收入) |
|
||||
|----------|--------------|---------------|
|
||||
| `ASSET`(资产) | 余额减少 | 余额增加 |
|
||||
| `LIABILITY`(负债) | 负债减少(还款) | 负债增加(消费/借款) |
|
||||
| `EQUITY`(权益) | 权益减少 | 权益增加 |
|
||||
|
||||
## 信用卡交易示例
|
||||
|
||||
| 交易 | 账户 | account_type | dc_flag | 余额变化 |
|
||||
|------|------|-------------|---------|---------|
|
||||
| 用信用卡买咖啡 ¥30 | 信用卡 | LIABILITY | DEBIT | 负债 +30 |
|
||||
| 从储蓄卡还信用卡 ¥5000 | 储蓄卡 | ASSET | DEBIT | 余额 -5000 |
|
||||
| 从储蓄卡还信用卡 ¥5000 | 信用卡 | LIABILITY | CREDIT | 负债 -5000 |
|
||||
|
||||
## 会计恒等式
|
||||
|
||||
同币种时校验:
|
||||
|
||||
- **CREDIT(收入)**: `posting_amt = txn_amt + surcharge_amt - comm_amt - disc_amt`
|
||||
- **DEBIT(支出)**: `posting_amt = txn_amt + surcharge_amt + comm_amt - disc_amt`
|
||||
|
||||
## 金额/币种对校验
|
||||
|
||||
可选的金额/币种对(`posting_amt/ccy`、`comm_amt/ccy`、`surcharge_amt/ccy`、`disc_amt/ccy`)必须同时为 `None` 或同时有值。
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
from typing import Annotated, Self
|
||||
from typing import Annotated, Literal, Self
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic_extra_types.country import CountryAlpha2
|
||||
@@ -128,33 +128,35 @@ class BaseTransaction(BaseModel):
|
||||
class MiscInTransaction(BaseTransaction):
|
||||
"""其他收入交易"""
|
||||
|
||||
txn_scene = "MISC_IN"
|
||||
txn_scene: Annotated[Literal["MISC_IN"], "交易场景"] = "MISC_IN"
|
||||
dc_flag: Annotated[DcFlag, "借贷方向"] = DcFlag.CREDIT
|
||||
|
||||
|
||||
class MiscOutTransaction(BaseTransaction):
|
||||
"""其他支出交易"""
|
||||
|
||||
txn_scene = "MISC_OUT"
|
||||
txn_scene: Annotated[Literal["MISC_OUT"], "交易场景"] = "MISC_OUT"
|
||||
dc_flag: Annotated[DcFlag, "借贷方向"] = DcFlag.DEBIT
|
||||
|
||||
|
||||
class PaymentTransaction(MiscOutTransaction):
|
||||
"""支付交易"""
|
||||
|
||||
txn_scene = "PAYMENT"
|
||||
txn_scene: Annotated[Literal["PAYMENT"], "交易场景"] = "PAYMENT"
|
||||
merchant_name: Annotated[str | None, "商户名称", Field(default=None)]
|
||||
|
||||
|
||||
class EcomPaymentTransaction(PaymentTransaction):
|
||||
"""电子商务支付交易"""
|
||||
|
||||
txn_scene = "ECOM_PAYMENT"
|
||||
txn_scene: Annotated[Literal["ECOM_PAYMENT"], "交易场景"] = "ECOM_PAYMENT"
|
||||
order_id: Annotated[str | None, "订单ID", Field(default=None)]
|
||||
|
||||
|
||||
class PosPaymentTransaction(PaymentTransaction):
|
||||
"""POS支付交易"""
|
||||
|
||||
txn_scene = "POS_PAYMENT"
|
||||
txn_scene: Annotated[Literal["POS_PAYMENT"], "交易场景"] = "POS_PAYMENT"
|
||||
geo: Annotated[
|
||||
tuple[CountryAlpha2] | tuple[CountryAlpha2, str] | None, "地理信息", Field(default=None)
|
||||
]
|
||||
@@ -163,7 +165,7 @@ class PosPaymentTransaction(PaymentTransaction):
|
||||
class ATMTransaction(BaseTransaction):
|
||||
"""ATM交易"""
|
||||
|
||||
txn_scene = "ATM"
|
||||
txn_scene: Annotated[Literal["ATM"], "交易场景"] = "ATM"
|
||||
geo: Annotated[
|
||||
tuple[CountryAlpha2] | tuple[CountryAlpha2, str] | None, "地理信息", Field(default=None)
|
||||
]
|
||||
@@ -172,7 +174,7 @@ class ATMTransaction(BaseTransaction):
|
||||
class TransferTransaction(BaseTransaction):
|
||||
"""转账交易"""
|
||||
|
||||
txn_scene = "TRANSFER"
|
||||
txn_scene: Annotated[Literal["TRANSFER"], "交易场景"] = "TRANSFER"
|
||||
|
||||
|
||||
type Transaction = Annotated[
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from models.account import (
|
||||
AccountType,
|
||||
BankAccount,
|
||||
CashAccount,
|
||||
EWalletAccount,
|
||||
)
|
||||
from models.ref import AccountRef
|
||||
|
||||
|
||||
def test_bank_account():
|
||||
"""测试银行账户模型"""
|
||||
# 正常创建(最简)
|
||||
bank = BankAccount(name="招行工资卡", issuer_name="招商银行", account_suffix="8888")
|
||||
assert bank.account_type == AccountType.ASSET
|
||||
assert bank.name == "招行工资卡"
|
||||
assert bank.issuer_name == "招商银行"
|
||||
assert bank.account_suffix == "8888"
|
||||
assert bank.account_number is None
|
||||
assert bank.sub_accounts == []
|
||||
assert bank.is_active is True
|
||||
assert bank.deleted_at is None
|
||||
|
||||
# 带可选字段
|
||||
ref = AccountRef(ref_id=bank.id)
|
||||
bank_full = BankAccount(
|
||||
name="招行工资卡",
|
||||
issuer_name="招商银行",
|
||||
account_suffix="8888",
|
||||
account_number="6225880000008888",
|
||||
sub_accounts=[ref],
|
||||
primary_currency="CNY",
|
||||
supported_currencies=["CNY", "HKD", "USD"],
|
||||
remark="主账户",
|
||||
)
|
||||
assert bank_full.account_number == "6225880000008888"
|
||||
assert len(bank_full.sub_accounts) == 1
|
||||
assert bank_full.primary_currency == "CNY"
|
||||
assert len(bank_full.supported_currencies) == 3
|
||||
|
||||
# 负债类型(信用卡)
|
||||
credit = BankAccount(
|
||||
name="招行信用卡",
|
||||
issuer_name="招商银行",
|
||||
account_suffix="1234",
|
||||
account_type=AccountType.LIABILITY,
|
||||
)
|
||||
assert credit.account_type == AccountType.LIABILITY
|
||||
|
||||
# 必填字段缺失
|
||||
with pytest.raises(ValidationError):
|
||||
BankAccount(name="", issuer_name="招商银行", account_suffix="8888")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
BankAccount(name="招行工资卡", issuer_name="", account_suffix="8888")
|
||||
|
||||
|
||||
def test_ewallet_account():
|
||||
"""测试电子钱包账户模型"""
|
||||
# 正常创建(最简)
|
||||
wallet = EWalletAccount(name="支付宝日常", platform="Alipay")
|
||||
assert wallet.account_type == AccountType.ASSET
|
||||
assert wallet.platform == "Alipay"
|
||||
assert wallet.account_id is None
|
||||
|
||||
# 带可选字段
|
||||
wallet_full = EWalletAccount(
|
||||
name="支付宝日常",
|
||||
platform="Alipay",
|
||||
account_id="138****0000",
|
||||
primary_currency="CNY",
|
||||
remark="日常消费",
|
||||
)
|
||||
assert wallet_full.account_id == "138****0000"
|
||||
|
||||
# platform 必填
|
||||
with pytest.raises(ValidationError):
|
||||
EWalletAccount(name="支付宝", platform="")
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
EWalletAccount(platform="Alipay")
|
||||
|
||||
|
||||
def test_cash_account():
|
||||
"""测试现金账户模型"""
|
||||
# 正常创建(最简)
|
||||
cash = CashAccount(name="家里现金")
|
||||
assert cash.account_type == AccountType.ASSET
|
||||
assert cash.location is None
|
||||
|
||||
# 带可选字段
|
||||
cash_full = CashAccount(name="家里现金", location="保险箱")
|
||||
assert cash_full.location == "保险箱"
|
||||
|
||||
# name 必填
|
||||
with pytest.raises(ValidationError):
|
||||
CashAccount(name="")
|
||||
|
||||
|
||||
def test_account_ref():
|
||||
"""测试账户引用"""
|
||||
from models.account import BankAccount
|
||||
from ulid import ULID
|
||||
|
||||
bank = BankAccount(name="测试账户", issuer_name="测试银行", account_suffix="0000")
|
||||
ref = AccountRef(ref_id=bank.id)
|
||||
assert ref.ref_type == "ACCOUNT"
|
||||
assert ref.ref_id == bank.id
|
||||
@@ -0,0 +1,113 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from models.channel import (
|
||||
CashChannel,
|
||||
ChannelType,
|
||||
EWalletChannel,
|
||||
PaymentCardChannel,
|
||||
)
|
||||
from models.ref import AccountRef
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def account_ref():
|
||||
"""创建账户引用 fixture"""
|
||||
from ulid import ULID
|
||||
|
||||
return AccountRef(ref_id=ULID())
|
||||
|
||||
|
||||
def test_payment_card_channel(account_ref):
|
||||
"""测试支付卡渠道模型"""
|
||||
# 正常创建
|
||||
card = PaymentCardChannel(
|
||||
ref_accounts=[account_ref],
|
||||
region="HK",
|
||||
issuer_name="HSBC HK",
|
||||
card_suffix="8335",
|
||||
card_brand="UnionPay",
|
||||
)
|
||||
assert card.channel_type == ChannelType.PAYMENT_CARD
|
||||
assert card.region == "HK"
|
||||
assert card.issuer_name == "HSBC HK"
|
||||
assert card.card_suffix == "8335"
|
||||
assert card.card_brand == "UnionPay"
|
||||
assert len(card.ref_accounts) == 1
|
||||
|
||||
# card_brand 可为空
|
||||
card_without_brand = PaymentCardChannel(
|
||||
ref_accounts=[account_ref],
|
||||
region="HK",
|
||||
issuer_name="AMEX HK",
|
||||
card_suffix="12345",
|
||||
)
|
||||
assert card_without_brand.card_brand is None
|
||||
|
||||
# card_suffix 至少一位
|
||||
with pytest.raises(ValidationError):
|
||||
PaymentCardChannel(
|
||||
ref_accounts=[account_ref], region="CN", issuer_name="渣打中国", card_suffix=""
|
||||
)
|
||||
|
||||
# issuer_name 至少一位
|
||||
with pytest.raises(ValidationError):
|
||||
PaymentCardChannel(
|
||||
ref_accounts=[account_ref], region="SG", issuer_name="", card_suffix="7431"
|
||||
)
|
||||
|
||||
# 无效国家代码
|
||||
with pytest.raises(ValidationError):
|
||||
PaymentCardChannel(
|
||||
ref_accounts=[account_ref], region="XX", issuer_name="Fake Bank", card_suffix="0000"
|
||||
)
|
||||
|
||||
# ref_accounts 至少一个
|
||||
with pytest.raises(ValidationError):
|
||||
PaymentCardChannel(ref_accounts=[], region="CN", issuer_name="招商银行", card_suffix="8888")
|
||||
|
||||
|
||||
def test_ewallet_channel(account_ref):
|
||||
"""测试电子钱包渠道模型"""
|
||||
# 正常创建
|
||||
wallet = EWalletChannel(
|
||||
ref_accounts=[account_ref],
|
||||
region="CN",
|
||||
platform="支付宝",
|
||||
platform_account_id="138****0000",
|
||||
sub_channel="余额",
|
||||
)
|
||||
assert wallet.channel_type == ChannelType.E_WALLET
|
||||
assert wallet.region == "CN"
|
||||
assert wallet.platform == "支付宝"
|
||||
assert wallet.platform_account_id == "138****0000"
|
||||
assert wallet.sub_channel == "余额"
|
||||
|
||||
# 可选字段可以为空
|
||||
wallet_minimal = EWalletChannel(ref_accounts=[account_ref], region="CN", platform="WeChat")
|
||||
assert wallet_minimal.platform_account_id is None
|
||||
assert wallet_minimal.sub_channel is None
|
||||
|
||||
# platform 至少一位
|
||||
with pytest.raises(ValidationError):
|
||||
EWalletChannel(ref_accounts=[account_ref], region="CN", platform="")
|
||||
|
||||
|
||||
def test_cash_channel(account_ref):
|
||||
"""测试现金渠道模型"""
|
||||
# 正常创建
|
||||
cash = CashChannel(ref_accounts=[account_ref])
|
||||
assert cash.channel_type == ChannelType.CASH
|
||||
assert len(cash.ref_accounts) == 1
|
||||
|
||||
# 多个关联账户
|
||||
cash_multi = CashChannel(ref_accounts=[account_ref, account_ref])
|
||||
assert len(cash_multi.ref_accounts) == 2
|
||||
|
||||
# ref_accounts 至少一个
|
||||
with pytest.raises(ValidationError):
|
||||
CashChannel(ref_accounts=[])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -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