|
@@ -9,6 +9,7 @@ from decimal import Decimal
|
|
|
from asyncmy.errors import IntegrityError
|
|
from asyncmy.errors import IntegrityError
|
|
|
|
|
|
|
|
from post_clear_check.chain import (
|
|
from post_clear_check.chain import (
|
|
|
|
|
+ CARD_DIMENSION,
|
|
|
CHAIN_ACCOUNT_ASSET,
|
|
CHAIN_ACCOUNT_ASSET,
|
|
|
CHAIN_BUY,
|
|
CHAIN_BUY,
|
|
|
CHAIN_CASH_CHANGE,
|
|
CHAIN_CASH_CHANGE,
|
|
@@ -18,6 +19,10 @@ from post_clear_check.chain import (
|
|
|
CHAIN_TRADE,
|
|
CHAIN_TRADE,
|
|
|
CHAIN_UNASSIGNED,
|
|
CHAIN_UNASSIGNED,
|
|
|
CHAIN_USER_ASSET,
|
|
CHAIN_USER_ASSET,
|
|
|
|
|
+ DIM_ISSUE,
|
|
|
|
|
+ DIM_ORDER,
|
|
|
|
|
+ DIM_SALES,
|
|
|
|
|
+ DIM_TRS,
|
|
|
UNASSIGNED_BASIS,
|
|
UNASSIGNED_BASIS,
|
|
|
)
|
|
)
|
|
|
from post_clear_check.feishu import CARD_SUMMARY, SendResult
|
|
from post_clear_check.feishu import CARD_SUMMARY, SendResult
|
|
@@ -380,8 +385,8 @@ def test_same_round_second_entry_does_not_insert_another_run() -> None:
|
|
|
assert {row["status"] for row in conn.results} == {"sent"}
|
|
assert {row["status"] for row in conn.results} == {"sent"}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_cards_split_by_chain_owner_and_key() -> None:
|
|
|
|
|
- """按「链路 + 归属 + 主键」分卡。同一主订单的两条链路是两张卡,资金变动行归资金账户。"""
|
|
|
|
|
|
|
+def test_cards_group_into_four_dimensions() -> None:
|
|
|
|
|
+ """一轮最多四张方向卡。报表行留在产生它的那一端,订单卡只做索引。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
sent: list[object] = []
|
|
sent: list[object] = []
|
|
@@ -390,53 +395,29 @@ def test_cards_split_by_chain_owner_and_key() -> None:
|
|
|
await _run(conn, _script(), [], sent)
|
|
await _run(conn, _script(), [], sent)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- assert _formulas(conn, "ORD-A", CHAIN_BUY) == ["订单A", "发行A"]
|
|
|
|
|
- assert _formulas(conn, "ORD-A", CHAIN_TRADE) == ["TRS002-A"]
|
|
|
|
|
- assert len(_cards_of(conn, "ORD-A")) == 2
|
|
|
|
|
- assert _formulas(conn, "ORD-A1", CHAIN_BUY) == ["别的编号"]
|
|
|
|
|
- assert _formulas(conn, "ORD-B", CHAIN_SELL) == ["订单B", "发行B"]
|
|
|
|
|
- assert _formulas(conn, "ORD-B", CHAIN_TRADE) == ["TRS-HR99"]
|
|
|
|
|
- # 资金变动归资金账户:行上同时有主订单编号也不进主订单卡。
|
|
|
|
|
- assert "销售带订单A" not in _formulas(conn, "ORD-A")
|
|
|
|
|
- assert _formulas(conn, "FA9", CHAIN_CASH_CHANGE) == ["销售带订单A"]
|
|
|
|
|
- assert _formulas(conn, "FA9", CHAIN_ACCOUNT_ASSET) == ["资金A"]
|
|
|
|
|
- assert _formulas(conn, "FA9", CHAIN_NOTE_HOLDING) == ["切片1", "切片2"]
|
|
|
|
|
- assert _formulas(conn, "U9", CHAIN_USER_ASSET) == ["用户A"]
|
|
|
|
|
- # 委托执行没有主订单编号、也没有用户账号,进未归链路,结论为错误。
|
|
|
|
|
- unassigned = _cards_of(conn, "2026-10-17", CHAIN_UNASSIGNED)
|
|
|
|
|
- assert [row["kind"] for row in unassigned] == ["clearing_batch"]
|
|
|
|
|
- assert unassigned[0]["verdict"] == "error"
|
|
|
|
|
- assert _formulas(conn, "2026-10-17", CHAIN_UNASSIGNED) == ["结论行没有归属:HR150不算"]
|
|
|
|
|
- detail_chains = [
|
|
|
|
|
- CHAIN_BUY,
|
|
|
|
|
- CHAIN_TRADE,
|
|
|
|
|
- CHAIN_BUY,
|
|
|
|
|
- CHAIN_SELL,
|
|
|
|
|
- CHAIN_TRADE,
|
|
|
|
|
- CHAIN_CASH_CHANGE,
|
|
|
|
|
- CHAIN_ACCOUNT_ASSET,
|
|
|
|
|
- CHAIN_NOTE_HOLDING,
|
|
|
|
|
- CHAIN_USER_ASSET,
|
|
|
|
|
- CHAIN_UNASSIGNED,
|
|
|
|
|
- ]
|
|
|
|
|
- # 概况按链路登记顺序先发,同一条链路只一张。明细顺序不变,排在概况后面。
|
|
|
|
|
- summary_chains = [
|
|
|
|
|
- CHAIN_BUY,
|
|
|
|
|
- CHAIN_SELL,
|
|
|
|
|
- CHAIN_CASH_CHANGE,
|
|
|
|
|
- CHAIN_ACCOUNT_ASSET,
|
|
|
|
|
- CHAIN_NOTE_HOLDING,
|
|
|
|
|
- CHAIN_TRADE,
|
|
|
|
|
- CHAIN_USER_ASSET,
|
|
|
|
|
- CHAIN_UNASSIGNED,
|
|
|
|
|
- ]
|
|
|
|
|
- assert [card.chain_name for card in sent] == summary_chains + detail_chains
|
|
|
|
|
- assert [card.card_kind for card in sent[: len(summary_chains)]] == [CARD_SUMMARY] * len(summary_chains)
|
|
|
|
|
- assert [card.record_no for card in sent] == [str(index) for index in range(1, len(sent) + 1)]
|
|
|
|
|
|
|
+ day = DAY.isoformat()
|
|
|
|
|
+ assert [card.chain_name for card in sent] == [DIM_ORDER, DIM_ISSUE, DIM_SALES, DIM_TRS]
|
|
|
|
|
+ assert [card.card_kind for card in sent] == [CARD_DIMENSION] * 4
|
|
|
|
|
+ assert [card.biz_key for card in sent] == [day] * 4
|
|
|
|
|
+ assert [card.record_no for card in sent] == ["1", "2", "3", "4"]
|
|
|
assert sent[0].batch_no == "IB-9"
|
|
assert sent[0].batch_no == "IB-9"
|
|
|
- stored = _cards_of(conn, "ORD-A", CHAIN_BUY)[0]
|
|
|
|
|
- assert stored["dedupe"] == dedupe_key(READY, "master_order", "ORD-A", chain_name=CHAIN_BUY)
|
|
|
|
|
|
|
+ order_formulas = _formulas(conn, day, DIM_ORDER)
|
|
|
|
|
+ assert "订单A" in order_formulas
|
|
|
|
|
+ assert "订单B" in order_formulas
|
|
|
|
|
+ assert "发行A" not in order_formulas
|
|
|
|
|
+ assert "销售带订单A" not in order_formulas
|
|
|
|
|
+ assert any(text.startswith("关联报表索引:OTC003") for text in order_formulas)
|
|
|
|
|
+ assert any("发行 OTC003:本轮主单 2 笔,正确 2" in row["calc_text"] for row in conn.lines)
|
|
|
|
|
+ assert "别的编号" not in order_formulas
|
|
|
|
|
+ assert _formulas(conn, day, DIM_ISSUE) == ["发行A", "发行B", "别的编号"]
|
|
|
|
|
+ assert _formulas(conn, day, DIM_SALES) == ["资金A", "切片1", "切片2", "销售带订单A"]
|
|
|
|
|
+ assert "结论行没有归属:HR150不算" in _formulas(conn, day, DIM_TRS)
|
|
|
|
|
+ assert "用户A" in _formulas(conn, day, DIM_TRS)
|
|
|
|
|
+ stored = _cards_of(conn, day, DIM_ORDER)[0]
|
|
|
|
|
+ assert stored["dedupe"] == dedupe_key(READY, CARD_DIMENSION, day, chain_name=DIM_ORDER)
|
|
|
assert str(stored["dedupe"]).split("\x1f")[-1] == rule_pack_version()
|
|
assert str(stored["dedupe"]).split("\x1f")[-1] == rule_pack_version()
|
|
|
|
|
+ assert conn.runs[0]["correct"] == 3
|
|
|
|
|
+ assert conn.runs[0]["error"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_card_without_lines_is_not_inserted() -> None:
|
|
def test_card_without_lines_is_not_inserted() -> None:
|
|
@@ -474,9 +455,7 @@ def test_failed_send_rerun_does_not_copy_lines() -> None:
|
|
|
async def sender(_settings: object, card: object) -> SendResult:
|
|
async def sender(_settings: object, card: object) -> SendResult:
|
|
|
calls.append(card.record_no)
|
|
calls.append(card.record_no)
|
|
|
resent.append((card.card_kind, card.resend))
|
|
resent.append((card.card_kind, card.resend))
|
|
|
- # 概况一次成功。明细第一次失败,重发时才成功,用来钉住「不复制结论行」。
|
|
|
|
|
- if card.card_kind == CARD_SUMMARY:
|
|
|
|
|
- return SendResult(ok=True, retryable=False, attempted=True)
|
|
|
|
|
|
|
+ # 方向卡第一次失败,重发才成功,用来钉住「不复制结论行」。
|
|
|
ok = card.resend
|
|
ok = card.resend
|
|
|
return SendResult(ok=ok, retryable=not ok, attempted=True)
|
|
return SendResult(ok=ok, retryable=not ok, attempted=True)
|
|
|
|
|
|
|
@@ -487,22 +466,19 @@ def test_failed_send_rerun_does_not_copy_lines() -> None:
|
|
|
script["sales"] = lambda _data: []
|
|
script["sales"] = lambda _data: []
|
|
|
script["trs"] = lambda _data: []
|
|
script["trs"] = lambda _data: []
|
|
|
await _run(conn, script, sender=sender)
|
|
await _run(conn, script, sender=sender)
|
|
|
- assert len(conn.lines) == 1
|
|
|
|
|
- detail = next(row for row in conn.results if row["kind"] == "master_order")
|
|
|
|
|
|
|
+ assert sum(1 for row in conn.lines if row["formula"] == "只有一行") == 1
|
|
|
|
|
+ detail = next(row for row in conn.results if row["kind"] == CARD_DIMENSION)
|
|
|
assert detail["status"] == "failed"
|
|
assert detail["status"] == "failed"
|
|
|
await _run(conn, script, sender=sender)
|
|
await _run(conn, script, sender=sender)
|
|
|
await _run(conn, script, sender=sender)
|
|
await _run(conn, script, sender=sender)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
assert len(conn.runs) == 1
|
|
assert len(conn.runs) == 1
|
|
|
- assert len(conn.lines) == 1
|
|
|
|
|
- assert conn.lines[0]["formula"] == "只有一行"
|
|
|
|
|
- detail = next(row for row in conn.results if row["kind"] == "master_order")
|
|
|
|
|
- summary = next(row for row in conn.results if row["kind"] == CARD_SUMMARY)
|
|
|
|
|
- assert calls == [str(summary["id"]), str(detail["id"]), str(detail["id"])]
|
|
|
|
|
- assert resent == [(CARD_SUMMARY, False), ("master_order", False), ("master_order", True)]
|
|
|
|
|
|
|
+ assert sum(1 for row in conn.lines if row["formula"] == "只有一行") == 1
|
|
|
|
|
+ detail = next(row for row in conn.results if row["kind"] == CARD_DIMENSION)
|
|
|
|
|
+ assert calls == [str(detail["id"]), str(detail["id"])]
|
|
|
|
|
+ assert resent == [(CARD_DIMENSION, False), (CARD_DIMENSION, True)]
|
|
|
assert detail["status"] == "sent"
|
|
assert detail["status"] == "sent"
|
|
|
- assert summary["status"] == "sent"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_verdict_is_error_when_suspect_and_error_share_a_card() -> None:
|
|
def test_verdict_is_error_when_suspect_and_error_share_a_card() -> None:
|
|
@@ -524,10 +500,10 @@ def test_verdict_is_error_when_suspect_and_error_share_a_card() -> None:
|
|
|
await _run(conn, script)
|
|
await _run(conn, script)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- details = [row for row in conn.results if row["kind"] == "master_order"]
|
|
|
|
|
|
|
+ details = [row for row in conn.results if row["kind"] == CARD_DIMENSION]
|
|
|
assert len(details) == 1
|
|
assert len(details) == 1
|
|
|
assert details[0]["verdict"] == "error"
|
|
assert details[0]["verdict"] == "error"
|
|
|
- assert len([row for row in conn.results if row["kind"] == CARD_SUMMARY]) == 1
|
|
|
|
|
|
|
+ assert details[0]["chain"] == DIM_ORDER
|
|
|
assert conn.runs[0]["error"] == 1
|
|
assert conn.runs[0]["error"] == 1
|
|
|
assert conn.runs[0]["suspect"] == 0
|
|
assert conn.runs[0]["suspect"] == 0
|
|
|
assert conn.runs[0]["correct"] == 0
|
|
assert conn.runs[0]["correct"] == 0
|
|
@@ -691,11 +667,12 @@ def test_assemble_puts_same_fund_notes_on_one_card() -> None:
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
(),
|
|
(),
|
|
|
|
|
+ business_date=DAY,
|
|
|
)
|
|
)
|
|
|
assert len(cards) == 1
|
|
assert len(cards) == 1
|
|
|
- assert cards[0].chain_name == CHAIN_NOTE_HOLDING
|
|
|
|
|
- assert cards[0].card_kind == "fund_account"
|
|
|
|
|
- assert cards[0].biz_key == "FA9"
|
|
|
|
|
|
|
+ assert cards[0].chain_name == DIM_SALES
|
|
|
|
|
+ assert cards[0].card_kind == CARD_DIMENSION
|
|
|
|
|
+ assert cards[0].biz_key == DAY.isoformat()
|
|
|
assert cards[0].verdict == "suspect"
|
|
assert cards[0].verdict == "suspect"
|
|
|
assert [line.formula_text for line in cards[0].lines] == ["切片1", "切片2"]
|
|
assert [line.formula_text for line in cards[0].lines] == ["切片1", "切片2"]
|
|
|
|
|
|
|
@@ -859,7 +836,7 @@ async def _run(
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gate_lines_share_the_batch_card() -> None:
|
|
def test_gate_lines_share_the_batch_card() -> None:
|
|
|
- """已完成轮次的是/否检查并进这一天的核对门禁,不另开一张 completed 卡。"""
|
|
|
|
|
|
|
+ """已完成轮次的是/否检查并进对应报表卡,不另开一张方向卡,也不再发核对门禁卡。"""
|
|
|
|
|
|
|
|
cards = assemble_cards(
|
|
cards = assemble_cards(
|
|
|
{},
|
|
{},
|
|
@@ -870,8 +847,8 @@ def test_gate_lines_share_the_batch_card() -> None:
|
|
|
gate_lines=(_line(FORMULA_ISSUE_PUBLISH, "应晚于本批营业日", verdict="error"),),
|
|
gate_lines=(_line(FORMULA_ISSUE_PUBLISH, "应晚于本批营业日", verdict="error"),),
|
|
|
)
|
|
)
|
|
|
assert len(cards) == 1
|
|
assert len(cards) == 1
|
|
|
- assert cards[0].chain_name == CHAIN_GATE
|
|
|
|
|
- assert cards[0].card_kind == "clearing_batch"
|
|
|
|
|
|
|
+ assert cards[0].chain_name == DIM_ISSUE
|
|
|
|
|
+ assert cards[0].card_kind == CARD_DIMENSION
|
|
|
assert cards[0].biz_key == DAY.isoformat()
|
|
assert cards[0].biz_key == DAY.isoformat()
|
|
|
assert cards[0].lines[0].formula_text == FORMULA_ISSUE_PUBLISH
|
|
assert cards[0].lines[0].formula_text == FORMULA_ISSUE_PUBLISH
|
|
|
assert "completed" not in cards[0].lines[0].formula_text
|
|
assert "completed" not in cards[0].lines[0].formula_text
|
|
@@ -888,8 +865,8 @@ def test_line_without_chain_is_kept_as_error() -> None:
|
|
|
business_date=DAY,
|
|
business_date=DAY,
|
|
|
)
|
|
)
|
|
|
assert len(cards) == 1
|
|
assert len(cards) == 1
|
|
|
- assert cards[0].chain_name == CHAIN_UNASSIGNED
|
|
|
|
|
- assert cards[0].card_kind == "clearing_batch"
|
|
|
|
|
|
|
+ assert cards[0].chain_name == DIM_ISSUE
|
|
|
|
|
+ assert cards[0].card_kind == CARD_DIMENSION
|
|
|
assert cards[0].biz_key == DAY.isoformat()
|
|
assert cards[0].biz_key == DAY.isoformat()
|
|
|
assert cards[0].verdict == "error"
|
|
assert cards[0].verdict == "error"
|
|
|
line = cards[0].lines[0]
|
|
line = cards[0].lines[0]
|
|
@@ -917,7 +894,7 @@ def test_line_with_empty_owner_key_is_kept_as_error() -> None:
|
|
|
business_date=DAY,
|
|
business_date=DAY,
|
|
|
)
|
|
)
|
|
|
assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [
|
|
assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [
|
|
|
- (CHAIN_UNASSIGNED, "clearing_batch", DAY.isoformat())
|
|
|
|
|
|
|
+ (DIM_ISSUE, CARD_DIMENSION, DAY.isoformat())
|
|
|
]
|
|
]
|
|
|
assert cards[0].verdict == "error"
|
|
assert cards[0].verdict == "error"
|
|
|
assert "票据存续要求的归属键为空" in cards[0].lines[0].calc_text
|
|
assert "票据存续要求的归属键为空" in cards[0].lines[0].calc_text
|
|
@@ -939,8 +916,12 @@ def test_trade_execution_falls_back_to_user_account() -> None:
|
|
|
chain_name=CHAIN_TRADE,
|
|
chain_name=CHAIN_TRADE,
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
|
|
+ business_date=DAY,
|
|
|
)
|
|
)
|
|
|
- assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [(CHAIN_TRADE, "trs_user", "U9")]
|
|
|
|
|
|
|
+ assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [
|
|
|
|
|
+ (DIM_TRS, CARD_DIMENSION, DAY.isoformat())
|
|
|
|
|
+ ]
|
|
|
|
|
+ assert cards[0].lines[0].user_account == "U9"
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_order_rules_follow_master_direction() -> None:
|
|
def test_order_rules_follow_master_direction() -> None:
|
|
@@ -952,8 +933,11 @@ def test_order_rules_follow_master_direction() -> None:
|
|
|
assert lines
|
|
assert lines
|
|
|
assert {line.chain_name for line in lines} == {CHAIN_SELL}
|
|
assert {line.chain_name for line in lines} == {CHAIN_SELL}
|
|
|
assert {line.master_order_no for line in lines} == {"ORD-S"}
|
|
assert {line.master_order_no for line in lines} == {"ORD-S"}
|
|
|
- cards = assemble_cards(collect_order_lines(result), (), (), ())
|
|
|
|
|
- assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [(CHAIN_SELL, "master_order", "ORD-S")]
|
|
|
|
|
|
|
+ cards = assemble_cards(collect_order_lines(result), (), (), (), business_date=DAY)
|
|
|
|
|
+ assert [(card.chain_name, card.card_kind, card.biz_key) for card in cards] == [
|
|
|
|
|
+ (DIM_ORDER, CARD_DIMENSION, DAY.isoformat())
|
|
|
|
|
+ ]
|
|
|
|
|
+ assert {line.chain_name for line in cards[0].lines if line.chain_name} >= {CHAIN_SELL}
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reference_orders_do_not_run_order_rules() -> None:
|
|
def test_reference_orders_do_not_run_order_rules() -> None:
|
|
@@ -1059,10 +1043,9 @@ def test_missing_table_becomes_error_card() -> None:
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
assert [row["formula"] for row in conn.lines] == ["业务表应存在"]
|
|
assert [row["formula"] for row in conn.lines] == ["业务表应存在"]
|
|
|
assert conn.lines[0]["check_text"] == "db_issue.report_trade_order"
|
|
assert conn.lines[0]["check_text"] == "db_issue.report_trade_order"
|
|
|
- detail = next(row for row in conn.results if row["kind"] == "clearing_batch")
|
|
|
|
|
|
|
+ detail = next(row for row in conn.results if row["kind"] == CARD_DIMENSION)
|
|
|
assert detail["verdict"] == "error"
|
|
assert detail["verdict"] == "error"
|
|
|
- assert detail["chain"] == CHAIN_GATE
|
|
|
|
|
- assert detail["kind"] == "clearing_batch"
|
|
|
|
|
|
|
+ assert detail["chain"] == DIM_ISSUE
|
|
|
assert conn.lines[0]["basis"].startswith("研发设计 ")
|
|
assert conn.lines[0]["basis"].startswith("研发设计 ")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1102,42 +1085,46 @@ def test_historical_trs_block_is_stored_once() -> None:
|
|
|
assert len(conn.runs) == 1
|
|
assert len(conn.runs) == 1
|
|
|
details = [row for row in conn.results if row["kind"] == "clearing_batch"]
|
|
details = [row for row in conn.results if row["kind"] == "clearing_batch"]
|
|
|
assert len(details) == 1
|
|
assert len(details) == 1
|
|
|
- assert len([row for row in conn.results if row["kind"] == CARD_SUMMARY]) == 1
|
|
|
|
|
|
|
+ assert [row for row in conn.results if row["kind"] == CARD_SUMMARY] == []
|
|
|
assert details[0]["verdict"] == "error"
|
|
assert details[0]["verdict"] == "error"
|
|
|
assert details[0]["biz"] == DAY.isoformat()
|
|
assert details[0]["biz"] == DAY.isoformat()
|
|
|
assert details[0]["chain"] == CHAIN_GATE
|
|
assert details[0]["chain"] == CHAIN_GATE
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_push_filter_leaves_other_verdicts_pending() -> None:
|
|
|
|
|
- """只配置错误时,正确卡不发,仍保持 pending,改配置后可以补发。"""
|
|
|
|
|
|
|
+def test_push_filter_does_not_drop_a_dimension() -> None:
|
|
|
|
|
+ """只配置错误时,四个方向仍然整张发送。截断一个方向会让正确的报表看不见。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
sent: list[object] = []
|
|
sent: list[object] = []
|
|
|
- settings = sample_settings().model_copy(update={"feishu_push": "错误"})
|
|
|
|
|
|
|
+ settings = sample_settings().model_copy(update={"feishu_push": "错误", "feishu_push_limit": 1})
|
|
|
|
|
|
|
|
async def scenario() -> None:
|
|
async def scenario() -> None:
|
|
|
script = _script()
|
|
script = _script()
|
|
|
- script["orders"] = lambda _result: {
|
|
|
|
|
- "ORD-A": [_order_line("对", "correct", "ORD-A")],
|
|
|
|
|
- "ORD-B": [_order_line("错", "error", "ORD-B")],
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ script["orders"] = lambda _result: {"ORD-A": [_order_line("对", "correct", "ORD-A")]}
|
|
|
script["issue"] = lambda _data: []
|
|
script["issue"] = lambda _data: []
|
|
|
- script["sales"] = lambda _data: []
|
|
|
|
|
|
|
+ script["sales"] = lambda _data: [
|
|
|
|
|
+ _line(
|
|
|
|
|
+ "资金对",
|
|
|
|
|
+ "OTC001 资金账户资金报表.余额 = 1.00(资金账户 FA9)",
|
|
|
|
|
+ "correct",
|
|
|
|
|
+ report_code="OTC001",
|
|
|
|
|
+ fund_account="FA9",
|
|
|
|
|
+ chain_name=CHAIN_ACCOUNT_ASSET,
|
|
|
|
|
+ )
|
|
|
|
|
+ ]
|
|
|
script["trs"] = lambda _data: []
|
|
script["trs"] = lambda _data: []
|
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- # 概况先发,而且不看推送配置。明细里只有错误卡发出,正确卡仍是 pending。
|
|
|
|
|
- assert [card.biz_key for card in sent] == [CHAIN_BUY, "ORD-B"]
|
|
|
|
|
- assert [card.card_kind for card in sent] == [CARD_SUMMARY, "master_order"]
|
|
|
|
|
- status = {row["biz"]: row["status"] for row in conn.results}
|
|
|
|
|
- assert status["ORD-A"] == "pending"
|
|
|
|
|
- assert status["ORD-B"] == "sent"
|
|
|
|
|
- assert status[CHAIN_BUY] == "sent"
|
|
|
|
|
|
|
+ assert [card.chain_name for card in sent] == [DIM_ORDER, DIM_SALES]
|
|
|
|
|
+ assert [card.card_kind for card in sent] == [CARD_DIMENSION, CARD_DIMENSION]
|
|
|
|
|
+ assert all(card.sample_index is None for card in sent)
|
|
|
|
|
+ status = {(row["chain"], row["status"]) for row in conn.results}
|
|
|
|
|
+ assert status == {(DIM_ORDER, "sent"), (DIM_SALES, "sent")}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_push_limit_samples_each_chain() -> None:
|
|
|
|
|
- """每条链路只发待发列表最前面的张数。另一条链路各自再抽,不共用名额。"""
|
|
|
|
|
|
|
+def test_push_limit_does_not_split_a_dimension() -> None:
|
|
|
|
|
+ """条数上限不再把一个方向拆成只发第一张。同一张订单卡里的四笔一起发出。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
sent: list[object] = []
|
|
sent: list[object] = []
|
|
@@ -1160,35 +1147,11 @@ def test_push_limit_samples_each_chain() -> None:
|
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- details = [card for card in sent if card.card_kind != CARD_SUMMARY]
|
|
|
|
|
- assert [(card.chain_name, card.biz_key, card.sample_index) for card in details] == [
|
|
|
|
|
- (CHAIN_BUY, "ORD-A", 1),
|
|
|
|
|
- (CHAIN_SELL, "ORD-C", 1),
|
|
|
|
|
- ]
|
|
|
|
|
- status = {row["biz"]: row["status"] for row in conn.results if row["kind"] == "master_order"}
|
|
|
|
|
- assert status == {
|
|
|
|
|
- "ORD-A": "sent",
|
|
|
|
|
- "ORD-B": "pending",
|
|
|
|
|
- "ORD-C": "sent",
|
|
|
|
|
- "ORD-D": "pending",
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async def again() -> None:
|
|
|
|
|
- script = _script()
|
|
|
|
|
- script["orders"] = orders
|
|
|
|
|
- script["issue"] = lambda _data: []
|
|
|
|
|
- script["sales"] = lambda _data: []
|
|
|
|
|
- script["trs"] = lambda _data: []
|
|
|
|
|
- await _run(conn, script, sent=sent, settings=settings)
|
|
|
|
|
-
|
|
|
|
|
- asyncio.run(again())
|
|
|
|
|
- details = [card for card in sent if card.card_kind != CARD_SUMMARY]
|
|
|
|
|
- assert [(card.chain_name, card.biz_key, card.sample_index) for card in details] == [
|
|
|
|
|
- (CHAIN_BUY, "ORD-A", 1),
|
|
|
|
|
- (CHAIN_SELL, "ORD-C", 1),
|
|
|
|
|
- (CHAIN_BUY, "ORD-B", 1),
|
|
|
|
|
- (CHAIN_SELL, "ORD-D", 1),
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ assert [card.chain_name for card in sent] == [DIM_ORDER]
|
|
|
|
|
+ assert sent[0].sample_index is None
|
|
|
|
|
+ formulas = {line.formula_text for line in sent[0].lines}
|
|
|
|
|
+ assert {"买1", "买2", "卖1", "卖2"} <= formulas
|
|
|
|
|
+ assert conn.results[0]["status"] == "sent"
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_push_limit_above_count_sends_all() -> None:
|
|
def test_push_limit_above_count_sends_all() -> None:
|
|
@@ -1210,11 +1173,12 @@ def test_push_limit_above_count_sends_all() -> None:
|
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- assert [card.biz_key for card in sent if card.card_kind != CARD_SUMMARY] == ["ORD-A", "ORD-B"]
|
|
|
|
|
|
|
+ assert [card.chain_name for card in sent if card.card_kind == CARD_DIMENSION] == [DIM_ORDER]
|
|
|
|
|
+ assert {line.formula_text for line in sent[0].lines} >= {"错1", "错2"}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_correct_chain_summary_is_sent_when_push_filter_is_error_only() -> None:
|
|
|
|
|
- """概况卡的总结论是正确时,只推错误的配置仍然先发概况。明细保持 pending。"""
|
|
|
|
|
|
|
+def test_correct_dimension_is_sent_when_push_filter_is_error_only() -> None:
|
|
|
|
|
+ """方向卡总结论是正确时,只推错误的配置仍然发送。三个计数按这张卡。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
sent: list[object] = []
|
|
sent: list[object] = []
|
|
@@ -1229,28 +1193,24 @@ def test_correct_chain_summary_is_sent_when_push_filter_is_error_only() -> None:
|
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
await _run(conn, script, sent=sent, settings=settings)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- assert [card.card_kind for card in sent] == [CARD_SUMMARY]
|
|
|
|
|
- assert sent[0].chain_name == CHAIN_BUY
|
|
|
|
|
- assert [(owner.biz_key, owner.verdict) for owner in sent[0].owners] == [("ORD-A", "correct")]
|
|
|
|
|
- assert sent[0].owners[0].lines == ()
|
|
|
|
|
- detail = next(row for row in conn.results if row["kind"] == "master_order")
|
|
|
|
|
- summary = next(row for row in conn.results if row["kind"] == CARD_SUMMARY)
|
|
|
|
|
- assert detail["status"] == "pending"
|
|
|
|
|
- assert summary["status"] == "sent"
|
|
|
|
|
- assert summary["verdict"] == "correct"
|
|
|
|
|
|
|
+ assert [card.card_kind for card in sent] == [CARD_DIMENSION]
|
|
|
|
|
+ assert sent[0].chain_name == DIM_ORDER
|
|
|
|
|
+ detail = conn.results[0]
|
|
|
|
|
+ assert detail["status"] == "sent"
|
|
|
|
|
+ assert detail["verdict"] == "correct"
|
|
|
assert conn.runs[0]["correct"] == 1
|
|
assert conn.runs[0]["correct"] == 1
|
|
|
assert conn.runs[0]["error"] == 0
|
|
assert conn.runs[0]["error"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_summary_resend_keeps_owner_without_rereading_lines() -> None:
|
|
|
|
|
- """概况失败、明细已发出时,重发概况仍带归属,不再读结论行。"""
|
|
|
|
|
|
|
+def test_dimension_resend_keeps_the_same_lines() -> None:
|
|
|
|
|
+ """方向卡失败后重发仍是这一张,不新插结论行。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
seen: list[object] = []
|
|
seen: list[object] = []
|
|
|
|
|
|
|
|
async def sender(_settings: object, card: object) -> SendResult:
|
|
async def sender(_settings: object, card: object) -> SendResult:
|
|
|
seen.append(card)
|
|
seen.append(card)
|
|
|
- if card.card_kind == CARD_SUMMARY and not card.resend:
|
|
|
|
|
|
|
+ if not card.resend:
|
|
|
return SendResult(ok=False, retryable=True, attempted=True)
|
|
return SendResult(ok=False, retryable=True, attempted=True)
|
|
|
return SendResult(ok=True, retryable=False, attempted=True)
|
|
return SendResult(ok=True, retryable=False, attempted=True)
|
|
|
|
|
|
|
@@ -1265,14 +1225,15 @@ def test_summary_resend_keeps_owner_without_rereading_lines() -> None:
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
resent = seen[-1]
|
|
resent = seen[-1]
|
|
|
- assert resent.card_kind == CARD_SUMMARY
|
|
|
|
|
|
|
+ assert resent.card_kind == CARD_DIMENSION
|
|
|
assert resent.resend is True
|
|
assert resent.resend is True
|
|
|
- assert resent.owners[0].biz_key == "ORD-A"
|
|
|
|
|
- assert resent.owners[0].lines == ()
|
|
|
|
|
|
|
+ assert resent.chain_name == DIM_ORDER
|
|
|
|
|
+ assert any(line.formula_text == "只有一行" for line in resent.lines)
|
|
|
|
|
+ assert sum(1 for row in conn.lines if row["formula"] == "只有一行") == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
-def test_existing_round_backfills_summary_before_pending_details() -> None:
|
|
|
|
|
- """旧轮次只有明细时,下次启动补插概况并先发。三个计数不因补插增加。"""
|
|
|
|
|
|
|
+def test_existing_round_does_not_backfill_a_summary() -> None:
|
|
|
|
|
+ """已经落下的旧归属卡下次只重发自己,不再补插概况,也不重算结论行。"""
|
|
|
|
|
|
|
|
conn = _Own()
|
|
conn = _Own()
|
|
|
sent: list[object] = []
|
|
sent: list[object] = []
|
|
@@ -1290,14 +1251,13 @@ def test_existing_round_backfills_summary_before_pending_details() -> None:
|
|
|
await _run(conn, script, sent=sent)
|
|
await _run(conn, script, sent=sent)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- assert [card.card_kind for card in sent] == [CARD_SUMMARY, "master_order"]
|
|
|
|
|
- assert [card.record_no for card in sent] == ["2", "1"]
|
|
|
|
|
- assert [(owner.biz_key, owner.verdict) for owner in sent[0].owners] == [("ORD-A", "error")]
|
|
|
|
|
|
|
+ assert [card.card_kind for card in sent] == ["master_order"]
|
|
|
|
|
+ assert sent[0].record_no == "1"
|
|
|
assert len(conn.lines) == 1
|
|
assert len(conn.lines) == 1
|
|
|
assert conn.lines[0]["formula"] == "只有一行"
|
|
assert conn.lines[0]["formula"] == "只有一行"
|
|
|
assert conn.runs[0]["error"] == 1
|
|
assert conn.runs[0]["error"] == 1
|
|
|
assert conn.runs[0]["correct"] == 0
|
|
assert conn.runs[0]["correct"] == 0
|
|
|
- assert len([row for row in conn.results if row["kind"] == CARD_SUMMARY]) == 1
|
|
|
|
|
|
|
+ assert [row for row in conn.results if row["kind"] == CARD_SUMMARY] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_completed_run_silences_unsent_snapshot() -> None:
|
|
def test_completed_run_silences_unsent_snapshot() -> None:
|
|
@@ -1335,7 +1295,7 @@ def test_completed_run_silences_unsent_snapshot() -> None:
|
|
|
await _run(conn, script, scan=GateScan(ready=(READY,), lines=()), sender=sender)
|
|
await _run(conn, script, scan=GateScan(ready=(READY,), lines=()), sender=sender)
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
- assert calls == ["1", "2"]
|
|
|
|
|
|
|
+ assert calls == ["1"]
|
|
|
assert {row["status"] for row in conn.results} == {"sent"}
|
|
assert {row["status"] for row in conn.results} == {"sent"}
|
|
|
detail = next(row for row in conn.results if row["kind"] == "clearing_batch")
|
|
detail = next(row for row in conn.results if row["kind"] == "clearing_batch")
|
|
|
assert detail["biz"] == "SB-OPEN"
|
|
assert detail["biz"] == "SB-OPEN"
|
|
@@ -1385,7 +1345,7 @@ def test_snapshot_table_change_adds_a_card() -> None:
|
|
|
asyncio.run(scenario())
|
|
asyncio.run(scenario())
|
|
|
assert len(conn.runs) == 1
|
|
assert len(conn.runs) == 1
|
|
|
assert len([row for row in conn.results if row["kind"] == "clearing_batch"]) == 2
|
|
assert len([row for row in conn.results if row["kind"] == "clearing_batch"]) == 2
|
|
|
- assert len([row for row in conn.results if row["kind"] == CARD_SUMMARY]) == 1
|
|
|
|
|
|
|
+ assert [row for row in conn.results if row["kind"] == CARD_SUMMARY] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_clip_marks_overflow_and_biz_key_is_stable() -> None:
|
|
def test_clip_marks_overflow_and_biz_key_is_stable() -> None:
|