- Introduced `pending_trade` handling in the chat API to manage trade requests more effectively. - Updated the `submit_trade_api` to allow advisors to access customer trades based on assigned roles. - Added new methods in `GatewayRepository` for managing core holdings during trade subscriptions and redemptions. - Implemented context-aware trade dialogue management in the customer service layer to improve user experience during multi-turn interactions. - Enhanced the tool service to support trade actions and suitability checks, ensuring accurate processing of user requests. This update significantly improves the trade interaction flow, providing a more robust and user-friendly experience for customers engaging in trading activities.
177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
import pytest
|
||
|
||
from app.repository.core_ro import CoreReadOnlyRepository
|
||
from app.service.trade_action_service import (
|
||
build_trade_draft,
|
||
looks_like_trade_execute,
|
||
should_use_suitability_instead_of_trade,
|
||
subscribe_suitability_block_text,
|
||
)
|
||
from app.service.trade_flow_service import (
|
||
clear_trade_flow,
|
||
process_trade_turn,
|
||
trade_dialogue_should_continue,
|
||
)
|
||
from app.tool.core_ro_tool import extract_r_levels, query_suitability
|
||
|
||
|
||
def test_reply_one_continues_trade_from_context_without_redis(monkeypatch):
|
||
class BrokenRedis:
|
||
def get(self, _key):
|
||
return None
|
||
|
||
def setex(self, *_a, **_k):
|
||
raise OSError("redis down")
|
||
|
||
def delete(self, *_a):
|
||
return 0
|
||
|
||
monkeypatch.setattr("app.service.trade_flow_service._redis", lambda: BrokenRedis())
|
||
ctx = (
|
||
"assistant: 请选择要申购的产品(仅列出与您风评匹配、可购买的产品):\n"
|
||
"1. 稳健债基 A(PROD-110022)\n"
|
||
"2. 现金宝货币(PROD-000001)\n"
|
||
"请回复序号(如 1);若都不对,请直接输入产品全称或代码。"
|
||
)
|
||
assert trade_dialogue_should_continue("sess-x", "1", ctx) is True
|
||
draft = process_trade_turn("sess-x", "CUST-9527", "1", ctx)
|
||
assert draft.get("ok") is False
|
||
hint = draft.get("hint") or ""
|
||
assert "暂时无法回答" not in hint
|
||
assert "稳健债基" in hint or "金额" in hint or "申购" in hint
|
||
|
||
|
||
def test_reply_one_continues_from_purchasable_catalog_footer(monkeypatch):
|
||
"""选购清单用「请回复上方序号」,须识别为交易菜单续轮(非 fallback)。"""
|
||
class BrokenRedis:
|
||
def get(self, _key):
|
||
return None
|
||
|
||
def setex(self, *_a, **_k):
|
||
raise OSError("redis down")
|
||
|
||
def delete(self, *_a):
|
||
return 0
|
||
|
||
monkeypatch.setattr("app.service.trade_flow_service._redis", lambda: BrokenRedis())
|
||
ctx = (
|
||
"assistant: 按您当前风评C3(平衡型),在售产品清单如下:\n"
|
||
"【R1】\n"
|
||
"1. 现金宝货币(PROD-000001)— 可直接申购\n"
|
||
"【R2】\n"
|
||
"2. 稳健债基(PROD-110022)— 可直接申购\n"
|
||
"\n请回复上方序号(如 2)选择要申购的产品。"
|
||
)
|
||
assert trade_dialogue_should_continue("sess-catalog", "1", ctx) is True
|
||
draft = process_trade_turn("sess-catalog", "CUST-9527", "1", ctx)
|
||
assert draft.get("ok") is False
|
||
hint = draft.get("hint") or ""
|
||
assert "暂时无法回答" not in hint
|
||
assert "稳健债基" in hint or "金额" in hint or "申购" in hint
|
||
|
||
|
||
def test_want_buy_one_asks_amount_not_one_yuan(monkeypatch):
|
||
class BrokenRedis:
|
||
def get(self, _key):
|
||
return None
|
||
|
||
def setex(self, *_a, **_k):
|
||
raise OSError("redis down")
|
||
|
||
def delete(self, *_a):
|
||
return 0
|
||
|
||
monkeypatch.setattr("app.service.trade_flow_service._redis", lambda: BrokenRedis())
|
||
ctx = (
|
||
"assistant: 在售产品清单如下:\n"
|
||
"1. 现金宝货币(PROD-000001)— 可直接申购\n"
|
||
"请回复上方序号(如 2)选择要申购的产品。"
|
||
)
|
||
draft = process_trade_turn("sess-loose", "CUST-9527", "我想买1", ctx)
|
||
assert draft.get("ok") is False
|
||
hint = draft.get("hint") or ""
|
||
assert "金额" in hint
|
||
assert draft.get("payload", {}).get("amount") != 1.0
|
||
|
||
|
||
def test_r4_catalog_with_subscribe_not_trade_execute():
|
||
msg = "有哪些r4产品我想申购"
|
||
assert should_use_suitability_instead_of_trade(msg) is True
|
||
assert looks_like_trade_execute(msg) is False
|
||
|
||
|
||
def test_medical_subscribe_runs_suitability_not_product_menu():
|
||
ro = CoreReadOnlyRepository()
|
||
draft = process_trade_turn(
|
||
"sess-med-1",
|
||
"CUST-9527",
|
||
"我想申购《医药健康精选》",
|
||
"",
|
||
ro=ro,
|
||
)
|
||
assert draft.get("ok") is False
|
||
hint = draft.get("hint") or ""
|
||
assert "请选择要申购的产品" not in hint
|
||
assert "金额" not in hint or "不支持在此直接" in hint
|
||
assert "理财师" in hint or "代理人" in hint or "转人工" in hint
|
||
|
||
|
||
def test_extract_r_levels_multi():
|
||
assert extract_r_levels("有哪些 r4 和 r5 产品") == ["R4", "R5"]
|
||
|
||
|
||
def test_query_suitability_lists_r4_r5_products():
|
||
ro = CoreReadOnlyRepository()
|
||
res = query_suitability("CUST-9527", user_message="有哪些R4和R5产品?", repo=ro)
|
||
assert res["ok"] is True
|
||
text = res["fact_text"]
|
||
assert "【R4】" in text
|
||
assert "【R5】" in text
|
||
assert "PROD-" in text
|
||
|
||
|
||
def test_subscribe_two_turn_style_combined():
|
||
ro = CoreReadOnlyRepository()
|
||
ctx = "用户:我要申购"
|
||
draft = process_trade_turn("sess-test-1", "CUST-9527", "稳健债基 A,申购1万元", ctx, ro=ro)
|
||
if not draft.get("ok"):
|
||
pytest.skip("product seed mismatch")
|
||
assert draft["payload"]["trade_type"] == "subscribe"
|
||
assert draft["payload"]["amount"] == 10000.0
|
||
|
||
|
||
def test_build_subscribe_with_chinese_product_name():
|
||
ro = CoreReadOnlyRepository()
|
||
draft = build_trade_draft("稳健债基 A,申购1万元", "CUST-9527", ro=ro)
|
||
if not draft.get("ok"):
|
||
pytest.skip("product seed mismatch")
|
||
assert draft["payload"]["trade_type"] == "subscribe"
|
||
|
||
|
||
def test_trade_flow_menu_pick(monkeypatch):
|
||
class FakeRedis:
|
||
def __init__(self):
|
||
self.store = {}
|
||
|
||
def get(self, key):
|
||
return self.store.get(key)
|
||
|
||
def setex(self, key, _ttl, val):
|
||
self.store[key] = val
|
||
|
||
def delete(self, key):
|
||
self.store.pop(key, None)
|
||
|
||
fake = FakeRedis()
|
||
monkeypatch.setattr("app.service.trade_flow_service._redis", lambda: fake)
|
||
|
||
sid = "sess-menu-1"
|
||
clear_trade_flow(sid)
|
||
ro = CoreReadOnlyRepository()
|
||
|
||
r1 = process_trade_turn(sid, "CUST-9527", "我要申购", "", ro=ro)
|
||
assert r1.get("ok") is False
|
||
assert "请选择" in r1.get("hint", "") or "金额" in r1.get("hint", "") or "产品" in r1.get("hint", "")
|
||
|
||
clear_trade_flow(sid)
|