🎉 init
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from annotated_types import MinLen
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_extra_types.currency_code import Currency
|
||||
from ulid import ULID
|
||||
|
||||
from .ref import AccountRef
|
||||
|
||||
|
||||
class AccountType(StrEnum):
|
||||
"""资金性质"""
|
||||
|
||||
ASSET = "ASSET"
|
||||
LIABILITY = "LIABILITY"
|
||||
EQUITY = "EQUITY"
|
||||
|
||||
|
||||
class BaseAccount(BaseModel):
|
||||
"""基础账户模型"""
|
||||
|
||||
id: ULID = Field(default_factory=ULID)
|
||||
name: Annotated[str, "用户自定义名称", MinLen(1)]
|
||||
account_type: AccountType
|
||||
primary_currency: Annotated[Currency | None, "主币种(可选)", Field(default=None)]
|
||||
supported_currencies: Annotated[
|
||||
list[Currency] | None, "支持的币种列表(None=全币种)", Field(default=None)
|
||||
]
|
||||
is_active: Annotated[bool, "是否启用", Field(default=True)]
|
||||
remark: Annotated[str | None, "备注", Field(default=None)]
|
||||
created_at: Annotated[datetime, "创建时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
updated_at: Annotated[datetime, "更新时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
deleted_at: Annotated[datetime | None, "软删除时间(None=未删除)", Field(default=None)]
|
||||
|
||||
|
||||
class BankAccount(BaseAccount):
|
||||
"""银行账户"""
|
||||
|
||||
account_type: Literal[AccountType.ASSET, AccountType.LIABILITY] = AccountType.ASSET
|
||||
issuer_name: Annotated[str, "银行名称", MinLen(1)]
|
||||
account_number: Annotated[str | None, "完整账户号(敏感信息)", Field(default=None)]
|
||||
account_suffix: Annotated[
|
||||
str | None, "账户号末尾部分,用于识别", MinLen(1), Field(default=None)
|
||||
]
|
||||
sub_accounts: Annotated[list[AccountRef], "子账户引用", Field(default_factory=list)]
|
||||
|
||||
|
||||
class EWalletAccount(BaseAccount):
|
||||
"""电子钱包账户"""
|
||||
|
||||
account_type: Literal[AccountType.ASSET] = AccountType.ASSET
|
||||
platform: Annotated[
|
||||
str,
|
||||
"平台名称: Alipay/WeChat/PayMe/AlipayHK/MPay/Octopus/Tap&Go/AlipayMO",
|
||||
MinLen(1),
|
||||
]
|
||||
account_id: Annotated[str | None, "平台账户ID", Field(default=None)]
|
||||
|
||||
|
||||
class CashAccount(BaseAccount):
|
||||
"""现金账户"""
|
||||
|
||||
account_type: Literal[AccountType.ASSET] = AccountType.ASSET
|
||||
location: Annotated[str | None, "存放位置描述", Field(default=None)]
|
||||
|
||||
|
||||
type Account = Annotated[
|
||||
BankAccount | EWalletAccount | CashAccount,
|
||||
Field(discriminator="account_type"),
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_types import MinLen
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_extra_types.country import CountryAlpha2
|
||||
from ulid import ULID
|
||||
|
||||
from .ref import AccountRef
|
||||
|
||||
|
||||
class ChannelType(StrEnum):
|
||||
"""渠道类型"""
|
||||
|
||||
PAYMENT_CARD = "PAYMENT_CARD"
|
||||
E_WALLET = "E_WALLET"
|
||||
CASH = "CASH"
|
||||
|
||||
|
||||
class PaymentInstrumentType(StrEnum):
|
||||
"""支付工具类型"""
|
||||
|
||||
CREDIT = "CREDIT"
|
||||
DEBIT = "DEBIT"
|
||||
|
||||
# 暂无实现的必要
|
||||
# PREPAID = "PREPAID"
|
||||
# BNPL = "BNPL"
|
||||
# CONSUMER_LOAN = "CONSUMER_LOAN"
|
||||
|
||||
|
||||
class BaseChannel(BaseModel):
|
||||
"""基础渠道模型"""
|
||||
|
||||
id: ULID = Field(default_factory=ULID)
|
||||
ref_accounts: Annotated[list[AccountRef], "关联的账户引用列表", MinLen(1)]
|
||||
created_at: Annotated[datetime, "创建时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
updated_at: Annotated[datetime, "更新时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
deleted_at: Annotated[datetime | None, "软删除时间(None=未删除)", Field(default=None)]
|
||||
|
||||
|
||||
class PaymentCardChannel(BaseChannel):
|
||||
"""支付卡渠道"""
|
||||
|
||||
channel_type = ChannelType.PAYMENT_CARD
|
||||
region: Annotated[CountryAlpha2, "发卡地/清算区域"]
|
||||
issuer_name: Annotated[str, "发卡方名称", MinLen(1)]
|
||||
card_type: Annotated[PaymentInstrumentType, "卡类型"]
|
||||
card_suffix: Annotated[str, "卡号末尾部分,可为后4/5位或完整卡号", MinLen(1)]
|
||||
card_brand: Annotated[
|
||||
str | None,
|
||||
"卡组织: UnionPay/Visa/Mastercard/JCB/AMEX/Octopus。通常可由完整卡号自动推断",
|
||||
Field(default=None),
|
||||
]
|
||||
|
||||
|
||||
class EWalletChannel(BaseChannel):
|
||||
"""电子钱包渠道"""
|
||||
|
||||
channel_type = ChannelType.E_WALLET
|
||||
region: Annotated[CountryAlpha2, "钱包所属区域"]
|
||||
platform: Annotated[
|
||||
str,
|
||||
"平台名: Alipay/WeChatPay/PayMe/AlipayHK/MPay/Octopus/AlipayMO",
|
||||
MinLen(1),
|
||||
]
|
||||
platform_account_id: Annotated[str | None, "平台账户ID/手机号掩码", Field(default=None)]
|
||||
|
||||
sub_channel: Annotated[
|
||||
str | None, "子渠道: 余额/花呗/信用卡快捷/八达通App等", Field(default=None)
|
||||
]
|
||||
sub_channel_type: Annotated[PaymentInstrumentType, "子渠道类型"]
|
||||
|
||||
|
||||
class CashChannel(BaseChannel):
|
||||
"""现金渠道"""
|
||||
|
||||
channel_type = ChannelType.CASH
|
||||
|
||||
|
||||
class TransferChannel(BaseChannel):
|
||||
"""转账渠道"""
|
||||
|
||||
channel_type = ChannelType.TRANSFER
|
||||
|
||||
|
||||
type Channel = Annotated[
|
||||
PaymentCardChannel | EWalletChannel | CashChannel,
|
||||
Field(discriminator="channel_type"),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from ulid import ULID
|
||||
|
||||
|
||||
class BaseRef(BaseModel):
|
||||
"""基础引用模型"""
|
||||
|
||||
ref_type: str
|
||||
ref_id: ULID
|
||||
|
||||
|
||||
class AccountRef(BaseRef):
|
||||
"""账户引用"""
|
||||
|
||||
ref_type: Literal["ACCOUNT"] = "ACCOUNT"
|
||||
|
||||
|
||||
class ChannelRef(BaseRef):
|
||||
"""渠道引用"""
|
||||
|
||||
ref_type: Literal["CHANNEL"] = "CHANNEL"
|
||||
|
||||
|
||||
class TransactionRef(BaseRef):
|
||||
"""交易引用"""
|
||||
|
||||
ref_type: Literal["TRANSACTION"] = "TRANSACTION"
|
||||
@@ -0,0 +1,187 @@
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
from typing import Annotated, Self
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic_extra_types.country import CountryAlpha2
|
||||
from pydantic_extra_types.currency_code import Currency
|
||||
from ulid import ULID
|
||||
|
||||
from .ref import ChannelRef, TransactionRef
|
||||
|
||||
|
||||
class TransactionStatus(StrEnum):
|
||||
"""交易状态"""
|
||||
|
||||
PENDING = "PENDING"
|
||||
COMPLETED = "COMPLETED"
|
||||
FAILED = "FAILED"
|
||||
REFUNDED = "REFUNDED"
|
||||
|
||||
|
||||
class DcFlag(StrEnum):
|
||||
"""借贷方向"""
|
||||
|
||||
DEBIT = "DEBIT"
|
||||
CREDIT = "CREDIT"
|
||||
|
||||
|
||||
class FxRate(BaseModel):
|
||||
"""汇率快照"""
|
||||
|
||||
from_ccy: Annotated[Currency, "源币种"]
|
||||
to_ccy: Annotated[Currency, "目标币种"]
|
||||
rate: Annotated[Decimal, "汇率"]
|
||||
|
||||
|
||||
class BaseTransaction(BaseModel):
|
||||
"""基础交易模型"""
|
||||
|
||||
id: ULID = Field(default_factory=ULID)
|
||||
version: Annotated[int, "乐观锁版本号", Field(default=0)]
|
||||
ref_transactions: Annotated[
|
||||
list[TransactionRef], "关联的交易引用列表", Field(default_factory=list)
|
||||
]
|
||||
txn_date: Annotated[datetime, "交易日期"]
|
||||
clearing_date: Annotated[datetime | None, "清算日期", Field(default=None)]
|
||||
posting_date: Annotated[datetime | None, "入账日期", Field(default=None)]
|
||||
created_at: Annotated[datetime, "创建时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
updated_at: Annotated[datetime, "更新时间", Field(default_factory=lambda: datetime.now(UTC))]
|
||||
deleted_at: Annotated[datetime | None, "软删除时间(None=未删除)", Field(default=None)]
|
||||
txn_amt: Annotated[Decimal, "交易金额"]
|
||||
txn_ccy: Annotated[Currency, "交易币种"]
|
||||
posting_amt: Annotated[Decimal | None, "入账金额", Field(default=None)]
|
||||
posting_ccy: Annotated[Currency | None, "入账币种", Field(default=None)]
|
||||
comm_amt: Annotated[Decimal | None, "手续费金额", Field(default=None)]
|
||||
comm_ccy: Annotated[Currency | None, "手续费币种", Field(default=None)]
|
||||
surcharge_amt: Annotated[Decimal | None, "服务费金额", Field(default=None)]
|
||||
surcharge_ccy: Annotated[Currency | None, "服务费币种", Field(default=None)]
|
||||
disc_amt: Annotated[Decimal | None, "折扣金额", Field(default=None)]
|
||||
disc_ccy: Annotated[Currency | None, "折扣币种", Field(default=None)]
|
||||
fx_rates: Annotated[list[FxRate] | None, "汇率快照列表", Field(default=None)]
|
||||
dc_flag: Annotated[DcFlag, "借贷方向"]
|
||||
ref_channel: Annotated[ChannelRef, "关联的渠道引用列表"]
|
||||
cp: Annotated[str | ChannelRef, "对手方(字符串描述或渠道引用)"]
|
||||
acq_inst: Annotated[str | None, "收单机构", Field(default=None)]
|
||||
clearing_network: Annotated[str | None, "清算网络", Field(default=None)]
|
||||
txn_sts: Annotated[TransactionStatus, "交易状态", Field(default=TransactionStatus.COMPLETED)]
|
||||
description: Annotated[str | None, "交易描述", Field(default=None)]
|
||||
memo: Annotated[str | None, "备注", Field(default=None)]
|
||||
raw_description: Annotated[str | None, "原始交易描述", Field(default=None)]
|
||||
raw_data: Annotated[dict | None, "原始数据", Field(default=None)]
|
||||
|
||||
txn_scene: Annotated[str | None, "交易场景", Field(default=None)]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_amt_ccy_pairs(self) -> Self:
|
||||
pairs = [
|
||||
("posting_amt", "posting_ccy"),
|
||||
("comm_amt", "comm_ccy"),
|
||||
("surcharge_amt", "surcharge_ccy"),
|
||||
("disc_amt", "disc_ccy"),
|
||||
]
|
||||
for amt_field, ccy_field in pairs:
|
||||
amt = getattr(self, amt_field)
|
||||
ccy = getattr(self, ccy_field)
|
||||
if (amt is None) != (ccy is None):
|
||||
msg = f"{amt_field} and {ccy_field} must both be None or both have values"
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_accounting_equation(self) -> Self:
|
||||
"""
|
||||
会计恒等式校验(同币种)
|
||||
|
||||
- CREDIT: posting_amt = txn_amt + surcharge_amt - comm_amt - disc_amt
|
||||
- DEBIT: posting_amt = txn_amt + surcharge_amt + comm_amt - disc_amt
|
||||
"""
|
||||
if self.posting_amt is None or self.posting_ccy != self.txn_ccy:
|
||||
return self
|
||||
|
||||
expected = self.txn_amt
|
||||
if self.surcharge_amt is not None and self.surcharge_ccy == self.txn_ccy:
|
||||
expected += self.surcharge_amt
|
||||
if self.disc_amt is not None and self.disc_ccy == self.txn_ccy:
|
||||
expected -= self.disc_amt
|
||||
|
||||
if self.dc_flag == DcFlag.CREDIT:
|
||||
if self.comm_amt is not None and self.comm_ccy == self.txn_ccy:
|
||||
expected -= self.comm_amt
|
||||
elif self.dc_flag == DcFlag.DEBIT:
|
||||
if self.comm_amt is not None and self.comm_ccy == self.txn_ccy:
|
||||
expected += self.comm_amt
|
||||
|
||||
if self.posting_amt != expected:
|
||||
msg = (
|
||||
f"Accounting equation mismatch ({self.dc_flag.value}): "
|
||||
f"posting_amt({self.posting_amt}) != "
|
||||
f"txn_amt({self.txn_amt}) + surcharge_amt({self.surcharge_amt})"
|
||||
f" {'+' if self.dc_flag == DcFlag.DEBIT else '-'} comm_amt({self.comm_amt})"
|
||||
f" - disc_amt({self.disc_amt})"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
|
||||
|
||||
class MiscInTransaction(BaseTransaction):
|
||||
"""其他收入交易"""
|
||||
|
||||
txn_scene = "MISC_IN"
|
||||
|
||||
|
||||
class MiscOutTransaction(BaseTransaction):
|
||||
"""其他支出交易"""
|
||||
|
||||
txn_scene = "MISC_OUT"
|
||||
|
||||
|
||||
class PaymentTransaction(MiscOutTransaction):
|
||||
"""支付交易"""
|
||||
|
||||
txn_scene = "PAYMENT"
|
||||
merchant_name: Annotated[str | None, "商户名称", Field(default=None)]
|
||||
|
||||
|
||||
class EcomPaymentTransaction(PaymentTransaction):
|
||||
"""电子商务支付交易"""
|
||||
|
||||
txn_scene = "ECOM_PAYMENT"
|
||||
order_id: Annotated[str | None, "订单ID", Field(default=None)]
|
||||
|
||||
|
||||
class PosPaymentTransaction(PaymentTransaction):
|
||||
"""POS支付交易"""
|
||||
|
||||
txn_scene = "POS_PAYMENT"
|
||||
geo: Annotated[
|
||||
tuple[CountryAlpha2] | tuple[CountryAlpha2, str] | None, "地理信息", Field(default=None)
|
||||
]
|
||||
|
||||
|
||||
class ATMTransaction(BaseTransaction):
|
||||
"""ATM交易"""
|
||||
|
||||
txn_scene = "ATM"
|
||||
geo: Annotated[
|
||||
tuple[CountryAlpha2] | tuple[CountryAlpha2, str] | None, "地理信息", Field(default=None)
|
||||
]
|
||||
|
||||
|
||||
class TransferTransaction(BaseTransaction):
|
||||
"""转账交易"""
|
||||
|
||||
txn_scene = "TRANSFER"
|
||||
|
||||
|
||||
type Transaction = Annotated[
|
||||
MiscInTransaction
|
||||
| MiscOutTransaction
|
||||
| TransferTransaction
|
||||
| PaymentTransaction
|
||||
| EcomPaymentTransaction
|
||||
| PosPaymentTransaction
|
||||
| ATMTransaction,
|
||||
Field(discriminator="txn_scene"),
|
||||
]
|
||||
Reference in New Issue
Block a user