AutoRpa/rpa_cart/tests/test_cart_job.py

452 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 加购班次:拆仓加车不插行、系统故障短码。
from contextlib import contextmanager
from datetime import datetime, timedelta
from cart_job import KIND_BUSINESS, KIND_OK, KIND_SYSTEM, AddOutcome, run_cart_batch
from cart_match import CartOffer
from cart_repo import STAMP_CLAIMING, STAMP_DONE, STAMP_SELECTION, MemoryCartRepo
HANGZHOU = "杭州小药药自营旗舰店"
GUANGDONG = "广东壹号药业有限公司"
YBM = 48
YAOEX = 66
# 构造一条待加购表 2 行。
def _row(**overrides) -> dict:
data = {
"fid": 1,
"oms_id": 2026100201,
"platform_type": YAOEX,
"supplier_name": GUANGDONG,
"bar_code": "06901234567890",
"purchase_qty": 7,
"status": 1,
"fail_reason": None,
"push_status": 0,
"update_user_name": STAMP_SELECTION,
"operate_type": 1,
}
data.update(overrides)
return data
# 构造一条能对上仓的加购候选。
def _offer(vendor: str, **overrides) -> CartOffer:
data = dict(
sku_id="S-" + vendor[:2],
vendor_raw=vendor,
vendor_norm=vendor,
stock=20.0,
price=90.0,
)
data.update(overrides)
return CartOffer(**data)
# 杭州 3 + 广东 7 两行都加车,表 2 仍两行。
def test_job_adds_split_rows_without_insert():
repo = MemoryCartRepo(
[
_row(fid=1, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
_row(fid=2, platform_type=YAOEX, supplier_name=GUANGDONG, purchase_qty=7),
]
)
added: list[tuple[int, str, int]] = []
# 杭州仓有货。
def search_ybm(bar_code: str):
assert bar_code == "06901234567890"
return [_offer(HANGZHOU, sku_id="HZ", stock=5.0)]
# 广东仓有货。
def search_yaoex(bar_code: str):
assert bar_code == "06901234567890"
return [_offer(GUANGDONG, sku_id="GD", stock=20.0)]
# 记下药帮忙加车。
def add_ybm(offer: CartOffer, qty: int):
added.append((YBM, offer.sku_id, qty))
return AddOutcome(kind=KIND_OK)
# 记下 1 号药城加车。
def add_yaoex(offer: CartOffer, qty: int):
added.append((YAOEX, offer.sku_id, qty))
return AddOutcome(kind=KIND_OK)
summary = run_cart_batch(
repo,
searchers={YBM: search_ybm, YAOEX: search_yaoex},
adders={YBM: add_ybm, YAOEX: add_yaoex},
login_errors={},
)
assert summary.claimed == 2
assert summary.added == 2
assert summary.failed == 0
assert added == [(YBM, "HZ", 3), (YAOEX, "GD", 7)]
assert list(repo.rows) == [1, 2]
assert repo.rows[1]["status"] == 1
assert repo.rows[2]["status"] == 1
assert repo.rows[1]["update_user_name"] == STAMP_DONE
assert repo.rows[2]["update_user_name"] == STAMP_DONE
assert repo.rows[1]["push_status"] == 0
# 同一 omsId 一仓成功一仓失败:失败行原因带未加齐,成功行不回退。
def test_job_flags_incomplete_oms_when_one_row_fails():
repo = MemoryCartRepo(
[
_row(fid=1, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
_row(fid=2, platform_type=YAOEX, supplier_name=GUANGDONG, purchase_qty=7),
]
)
# 杭州仓有货。
def search_ybm(bar_code: str):
return [_offer(HANGZHOU, stock=5.0)]
# 广东仓库存不够。
def search_yaoex(bar_code: str):
return [_offer(GUANGDONG, stock=1.0)]
summary = run_cart_batch(
repo,
searchers={YBM: search_ybm, YAOEX: search_yaoex},
adders={
YBM: lambda offer, qty: AddOutcome(kind=KIND_OK),
YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK),
},
login_errors={},
)
assert summary.added == 1
assert summary.failed == 1
assert repo.rows[1]["status"] == 1
assert repo.rows[1]["fail_reason"] is None
assert repo.rows[2]["status"] == 2
assert repo.rows[2]["fail_reason"] == "未加齐:库存不足"
# 库存不够时该行 status=2,原因是库存不足。
def test_job_marks_out_of_stock():
repo = MemoryCartRepo([_row(purchase_qty=10)])
# 库存只有 4。
def search_yaoex(bar_code: str):
return [_offer(GUANGDONG, stock=4.0)]
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
login_errors={},
)
assert summary.added == 0
assert summary.failed == 1
assert repo.rows[1]["status"] == 2
assert repo.rows[1]["fail_reason"] == "库存不足"
assert repo.rows[1]["update_user_name"] == STAMP_DONE
assert repo.shift_errors == []
def test_job_marks_login_fail_as_system_error():
"""登录失败把戳改回系统管理员,status 保持 1,下一班还能领。"""
repo = MemoryCartRepo(
[
_row(fid=1, platform_type=YAOEX),
_row(fid=2, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
]
)
ybm_added = []
# 杭州仓有货。
def search_ybm(bar_code: str):
return [_offer(HANGZHOU)]
# 记下药帮忙加车数量。
def add_ybm(offer: CartOffer, qty: int):
ybm_added.append(qty)
return AddOutcome(kind=KIND_OK)
summary = run_cart_batch(
repo,
searchers={YBM: search_ybm},
adders={YBM: add_ybm},
login_errors={YAOEX: "账号密码错误"},
)
assert summary.added == 1
assert summary.failed == 1
assert repo.rows[1]["status"] == 1
assert repo.rows[1]["update_user_name"] == STAMP_SELECTION
assert repo.rows[1]["fail_reason"] == "未加齐:系统故障:LOGIN_FAILED"
assert "账号密码错误" not in repo.rows[1]["fail_reason"]
assert len(repo.shift_errors) == 1
assert repo.shift_errors[0].shift_name == "加购"
assert repo.shift_errors[0].biz_key == "1"
assert repo.shift_errors[0].error_code == "LOGIN_FAILED"
assert repo.shift_errors[0].error_text == "账号密码错误"
assert repo.rows[2]["status"] == 1
assert repo.rows[2]["update_user_name"] == STAMP_DONE
assert ybm_added == [3]
again = repo.claim_pending()
assert [row.fid for row in again] == [1]
# 已盖加购班的行本班不领。
def test_job_skips_already_stamped_rows():
repo = MemoryCartRepo([_row(update_user_name=STAMP_DONE)])
called = []
# 不应被调用。
def search_yaoex(bar_code: str):
called.append(bar_code)
return [_offer(GUANGDONG)]
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
login_errors={},
)
assert summary.claimed == 0
assert called == []
assert repo.rows[1]["update_user_name"] == STAMP_DONE
# 对不上 supplier_name 时写未找到对应仓。
def test_job_marks_missing_warehouse():
repo = MemoryCartRepo([_row()])
# 搜到的是杭州仓,对不上广东。
def search_yaoex(bar_code: str):
return [_offer(HANGZHOU)]
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
login_errors={},
)
assert summary.failed == 1
assert repo.rows[1]["status"] == 2
assert repo.rows[1]["fail_reason"] == "未找到对应仓"
# 平台返回业务失败时原样写下原因。
def test_job_records_mall_add_failure():
repo = MemoryCartRepo([_row()])
# 广东仓有货。
def search_yaoex(bar_code: str):
return [_offer(GUANGDONG)]
# 平台返回中包装加不进。
def add_yaoex(offer: CartOffer, qty: int):
return AddOutcome(kind=KIND_BUSINESS, reason="中包装加不进:数量须为10的倍数")
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: add_yaoex},
login_errors={},
)
assert summary.failed == 1
assert repo.rows[1]["fail_reason"] == "中包装加不进:数量须为10的倍数"
# 只跑一个平台时另一平台行仍停在系统管理员。
def test_job_platform_filter_leaves_other_mall_pending():
repo = MemoryCartRepo(
[
_row(fid=1, platform_type=YAOEX),
_row(fid=2, platform_type=YBM, supplier_name=HANGZHOU, purchase_qty=3),
]
)
# 广东仓有货。
def search_yaoex(bar_code: str):
return [_offer(GUANGDONG)]
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
login_errors={},
platform_types=[YAOEX],
)
assert summary.claimed == 1
assert summary.added == 1
assert repo.rows[1]["update_user_name"] == STAMP_DONE
assert repo.rows[2]["update_user_name"] == STAMP_SELECTION
assert repo.rows[2]["status"] == 1
def test_job_search_error_is_system_fail():
"""查询失败原因列只写 QUERY_FAILED,戳改回系统管理员,status 保持 1。"""
repo = MemoryCartRepo([_row()])
# 抛带原文的查询错。
def search_yaoex(bar_code: str):
raise RuntimeError("网关 502 secret-traceback")
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: lambda offer, qty: AddOutcome(kind=KIND_OK)},
login_errors={},
)
assert summary.failed == 1
reason = repo.rows[1]["fail_reason"]
assert reason == "系统故障:QUERY_FAILED"
assert repo.rows[1]["status"] == 1
assert repo.rows[1]["update_user_name"] == STAMP_SELECTION
assert "网关 502" not in reason
assert "secret-traceback" not in reason
assert len(repo.shift_errors) == 1
assert repo.shift_errors[0].error_code == "QUERY_FAILED"
assert "网关 502" in repo.shift_errors[0].error_text
assert "secret-traceback" in repo.shift_errors[0].error_text
# 加车抛错时原因列只写 ADD_FAILED,不含异常原文。
def test_job_add_error_writes_short_code_not_exception():
repo = MemoryCartRepo([_row()])
# 广东仓有货。
def search_yaoex(bar_code: str):
return [_offer(GUANGDONG)]
# 抛带 ORA 原文的加车错。
def add_yaoex(offer, qty: int):
raise RuntimeError("ORA-03114 secret-traceback")
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: add_yaoex},
login_errors={},
)
assert summary.failed == 1
reason = repo.rows[1]["fail_reason"]
assert reason == "系统故障:ADD_FAILED"
assert "ORA-03114" not in reason
assert "secret-traceback" not in reason
assert len(repo.shift_errors) == 1
assert repo.shift_errors[0].error_code == "ADD_FAILED"
assert "ORA-03114" in repo.shift_errors[0].error_text
class HoldingCartRepo(MemoryCartRepo):
"""内存仓库加上可计数的 session,用来断言商城调用期间连接已归还。"""
def __init__(self, rows):
"""在内存行上加 held 计数。"""
super().__init__(rows)
self.held = 0
@contextmanager
def session(self):
"""借连接时 held 加一,退出时减回。"""
self.held += 1
try:
yield
finally:
self.held -= 1
def test_mall_runs_after_connection_is_returned():
"""两组加车时连接已归还,盖章时才再借。"""
repo = HoldingCartRepo(
[
_row(fid=1, oms_id=2026100201),
_row(fid=2, oms_id=2026100202),
]
)
during_add: list[int] = []
during_mark: list[int] = []
original_claim = repo.claim_pending
original_success = repo.mark_success
original_errors = repo.save_shift_errors
def claim_pending(limit=None, platform_types=None, **kwargs):
"""领取时必须占着本班的连接。"""
assert repo.held == 1
return original_claim(limit=limit, platform_types=platform_types, **kwargs)
def mark_success(fid):
"""盖章时必须占着本班的连接。"""
during_mark.append(repo.held)
return original_success(fid)
def save_shift_errors(items):
"""审计走连接池,不能在 session 里写。"""
assert repo.held == 0
return original_errors(items)
def search_yaoex(_bar_code: str):
"""返回能对上仓的候选。"""
return [_offer(GUANGDONG)]
def add_yaoex(_offer, _qty: int):
"""加车时连接必须已经归还。"""
during_add.append(repo.held)
return AddOutcome(kind=KIND_OK)
repo.claim_pending = claim_pending
repo.mark_success = mark_success
repo.save_shift_errors = save_shift_errors
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: add_yaoex},
login_errors={},
)
assert during_add == [0, 0]
assert during_mark == [1, 1]
assert repo.held == 0
assert summary.added == 2
def test_rerun_skips_add_when_cart_already_has_the_line():
"""对方购物车已有不少于本行的数量、本地仍是加购中时,重跑不加第二份。"""
repo = MemoryCartRepo(
[
_row(
update_user_name=STAMP_CLAIMING,
update_time=datetime.now() - timedelta(minutes=31),
purchase_qty=7,
)
]
)
added: list[int] = []
def search_yaoex(_bar_code: str):
"""购物车已够时不应再搜。"""
raise AssertionError("购物车已够,不应再搜")
def add_yaoex(_offer, qty: int):
"""购物车已够时不应再加。"""
added.append(qty)
return AddOutcome(kind=KIND_OK)
def cart_qty(bar_code: str, supplier: str) -> int:
"""这一仓比本行多。多出来的数量不当成另一张单。"""
assert bar_code == "06901234567890"
assert supplier == GUANGDONG
return 10
summary = run_cart_batch(
repo,
searchers={YAOEX: search_yaoex},
adders={YAOEX: add_yaoex},
login_errors={},
cart_readers={YAOEX: cart_qty},
)
assert added == []
assert summary.added == 1
assert summary.failed == 0
assert repo.rows[1]["status"] == 1
assert repo.rows[1]["update_user_name"] == STAMP_DONE
assert repo.rows[1]["fail_reason"] is None