108 lines
4.6 KiB
Python
108 lines
4.6 KiB
Python
"""core_tools 查询口径单测(T-11 · FR-C15 + 归零行过滤)。
|
||||
|
|
|
|||
|
|
自建 sqlite 完整种子(`core_product` / `core_holding` / `core_trade`),直调 Tool 函数。
|
|||
|
|
覆盖 T-11 两条 DoD:
|
|||
|
|
|
|||
|
|
1. `query_recent_trades` 的 `sum_amount` 不因 convert 两条流水而**翻倍**
|
|||
|
|
(走 `amount_view`;明细与 `total_count` 仍为全量),
|
|||
|
|
并与 `core_ro.sum_trades_on_date` 做**跨口径一致性**断言(自检第 13 问:
|
|||
|
|
同一口径不得有两份实现漂移);
|
|||
|
|
2. `query_holdings` 不返回 `qty = 0` 的**归零行**(convert 转出全部份额后的台账留痕行)。
|
|||
|
|
|
|||
|
|
种子时间刻意取**同一个 `now`**:既落在 `query_recent_trades` 的 [now−30d, now) 窗内,
|
|||
|
|
又保证属于 `now.date()` 这一天,使跨口径断言不受运行时刻影响。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import datetime as _dt
|
|||
|
|
from decimal import Decimal
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from sqlalchemy import text
|
|||
|
|
|
|||
|
|
from _ddl import create_sqlite_engine
|
|||
|
|
|
|||
|
|
from app.repository.core_ro import CoreReadOnlyRepository
|
|||
|
|
from app.tool.core_tools import query_holdings, query_recent_trades
|
|||
|
|
|
|||
|
|
CUST = "CUST-T11"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture()
|
|||
|
|
def seed():
|
|||
|
|
engine = create_sqlite_engine()
|
|||
|
|
now = _dt.datetime.now()
|
|||
|
|
with engine.begin() as conn:
|
|||
|
|
conn.execute(
|
|||
|
|
text(
|
|||
|
|
"INSERT INTO core_product (product_id, product_name, min_risk_code, product_type,"
|
|||
|
|
" min_subscribe_amount, term_days) VALUES"
|
|||
|
|
" ('PA', '甲基金', 'R3', 'mixed', 100, 0),"
|
|||
|
|
" ('PB', '乙基金', 'R3', 'mixed', 100, 0)"
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
# PA 正常持有;PB 为 convert 转出全部后的**归零行**(qty = 0,行保留)
|
|||
|
|
conn.execute(
|
|||
|
|
text(
|
|||
|
|
"INSERT INTO core_holding (customer_id, product_id, qty, cost_amount,"
|
|||
|
|
" market_value, pnl_pct, as_of) VALUES"
|
|||
|
|
" (:cid, 'PA', 1000.00, 1000.00, 1200.00, 0.2000, :d),"
|
|||
|
|
" (:cid, 'PB', 0.00, 0.00, 0.00, 0.0000, :d)"
|
|||
|
|
),
|
|||
|
|
{"cid": CUST, "d": now.date()},
|
|||
|
|
)
|
|||
|
|
# 一次 convert 落两条(转出 redeem 300000 + 转入 subscribe 300000,共享 G1)
|
|||
|
|
# + 1 笔普通赎回 100000(convert_group_id = NULL)
|
|||
|
|
conn.execute(
|
|||
|
|
text(
|
|||
|
|
"INSERT INTO core_trade (trade_id, customer_id, product_id, trade_type, amount,"
|
|||
|
|
" qty, convert_group_id, trade_status, traded_at) VALUES"
|
|||
|
|
" ('TX-OUT', :cid, 'PA', 'redeem', 300000, 250.00, 'G1', 'confirmed', :t),"
|
|||
|
|
" ('TX-IN', :cid, 'PB', 'subscribe', 300000, 240.00, 'G1', 'confirmed', :t),"
|
|||
|
|
" ('TX-PLAIN', :cid, 'PA', 'redeem', 100000, 80.00, NULL, 'confirmed', :t)"
|
|||
|
|
),
|
|||
|
|
{"cid": CUST, "t": now},
|
|||
|
|
)
|
|||
|
|
yield engine
|
|||
|
|
engine.dispose()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_query_recent_trades_sum_not_doubled(seed):
|
|||
|
|
"""FR-C15:汇总只计转出端;**明细与条数保持全量**(一次转换两条是真实的)。"""
|
|||
|
|
repo = CoreReadOnlyRepository(engine=seed)
|
|||
|
|
res = query_recent_trades(CUST, days=30, core_ro=repo)
|
|||
|
|
|
|||
|
|
# 明细全量:convert 两条 + 普通赎回 1 条
|
|||
|
|
assert res["total_count"] == 3
|
|||
|
|
assert sorted(r["trade_type"] for r in res["items"]) == ["redeem", "redeem", "subscribe"]
|
|||
|
|
|
|||
|
|
# 汇总去重:300000(转出端)+ 100000(普通赎回)= 400000
|
|||
|
|
# 若未去重则为 700000(TX-OUT + TX-IN 双计)—— 用具体值才区分得开
|
|||
|
|
assert res["sum_amount"] == 400000.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_query_recent_trades_sum_matches_sum_trades_on_date(seed):
|
|||
|
|
"""跨口径一致性:Tool 汇总(Python 侧 amount_view)== 仓储 SQL 汇总(IS NULL OR ='')。
|
|||
|
|
|
|||
|
|
两处是同一口径的两种落地(一个 Python 一个 SQL),此处用断言锁死等价性,
|
|||
|
|
任一侧口径漂移都会在此变红。
|
|||
|
|
"""
|
|||
|
|
repo = CoreReadOnlyRepository(engine=seed)
|
|||
|
|
tool_sum = query_recent_trades(CUST, days=30, core_ro=repo)["sum_amount"]
|
|||
|
|
sql_sum = repo.sum_trades_on_date(CUST, _dt.date.today())
|
|||
|
|
assert Decimal(str(tool_sum)) == sql_sum
|
|||
|
|
assert sql_sum == Decimal("400000")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_query_holdings_excludes_zero_qty(seed):
|
|||
|
|
"""归零行(convert 转出全部份额,`qty = 0` 台账留痕)不得作为持仓返回。"""
|
|||
|
|
repo = CoreReadOnlyRepository(engine=seed)
|
|||
|
|
res = query_holdings(CUST, core_ro=repo)
|
|||
|
|
|
|||
|
|
assert res["total_count"] == 1
|
|||
|
|
assert [r["product_id"] for r in res["items"]] == ["PA"]
|
|||
|
|
# 合计不含归零行(PB 的 market_value 为 0,即使计入也不变;
|
|||
|
|
# 故断言**条数**才是真正能区分对错的判据)
|
|||
|
|
assert res["sum_market_value"] == 1200.0
|