550 lines
17 KiB
Python
550 lines
17 KiB
Python
# 加购班次:拆仓加车不插行、系统故障短码。
|
||
|
||
from contextlib import contextmanager
|
||
from datetime import datetime, timedelta
|
||
|
||
from cart_job import (
|
||
KIND_BUSINESS,
|
||
KIND_OK,
|
||
KIND_SYSTEM,
|
||
AddOutcome,
|
||
KnownLine,
|
||
add_known_lines,
|
||
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
|
||
|
||
|
||
def _known(fid: int, sku: str, **overrides) -> KnownLine:
|
||
"""一条已经拆定的加购行。raw 里的 skuId 必须跟 sku 一起交到加车。"""
|
||
data = dict(
|
||
fid=fid,
|
||
oms_id=2026100201,
|
||
platform_type=YBM,
|
||
supplier_name=HANGZHOU,
|
||
bar_code="06901234567890",
|
||
qty=5,
|
||
sku_id=sku,
|
||
vendor_raw=HANGZHOU,
|
||
vendor_norm=HANGZHOU,
|
||
stock=5.0,
|
||
price=90.0,
|
||
raw={"skuId": sku},
|
||
)
|
||
data.update(overrides)
|
||
return KnownLine(**data)
|
||
|
||
|
||
# 同仓两条 sku 各加各的,不按商家名并到低价那条。一条失败时只给失败行标未加齐。
|
||
def test_known_lines_keep_each_sku_and_flag_incomplete():
|
||
repo = MemoryCartRepo(
|
||
[
|
||
_row(
|
||
fid=11,
|
||
platform_type=YBM,
|
||
supplier_name=HANGZHOU,
|
||
purchase_qty=6,
|
||
update_user_name=STAMP_CLAIMING,
|
||
),
|
||
_row(
|
||
fid=12,
|
||
platform_type=YBM,
|
||
supplier_name=HANGZHOU,
|
||
purchase_qty=4,
|
||
update_user_name=STAMP_CLAIMING,
|
||
),
|
||
]
|
||
)
|
||
added: list[tuple[str, int]] = []
|
||
|
||
# 高价那条被商城拒绝。
|
||
def add_ybm(offer: CartOffer, qty: int):
|
||
added.append((offer.sku_id, qty))
|
||
assert offer.extra["raw"]["skuId"] == offer.sku_id
|
||
if offer.sku_id == "DEAR":
|
||
return AddOutcome(kind=KIND_BUSINESS, reason="库存不足")
|
||
return AddOutcome(kind=KIND_OK)
|
||
|
||
summary = add_known_lines(
|
||
repo,
|
||
[_known(11, "CHEAP", qty=6), _known(12, "DEAR", qty=4, price=92.0)],
|
||
adders={YBM: add_ybm},
|
||
login_errors={},
|
||
)
|
||
assert added == [("CHEAP", 6), ("DEAR", 4)]
|
||
assert summary.added == 1
|
||
assert summary.failed == 1
|
||
assert repo.rows[11]["status"] == 1
|
||
assert repo.rows[11]["update_user_name"] == STAMP_DONE
|
||
assert repo.rows[12]["status"] == 2
|
||
assert repo.rows[12]["fail_reason"] == "未加齐:库存不足"
|
||
|
||
|
||
# 没配加车函数时放回系统管理员,status 仍是 1。
|
||
def test_known_line_without_adder_releases_for_retry():
|
||
repo = MemoryCartRepo([_row(fid=1, update_user_name=STAMP_CLAIMING)])
|
||
summary = add_known_lines(
|
||
repo,
|
||
[
|
||
_known(
|
||
1,
|
||
"S1",
|
||
platform_type=YAOEX,
|
||
supplier_name=GUANGDONG,
|
||
vendor_raw=GUANGDONG,
|
||
vendor_norm=GUANGDONG,
|
||
)
|
||
],
|
||
adders={},
|
||
login_errors={},
|
||
)
|
||
assert summary.added == 0
|
||
assert summary.failed == 1
|
||
assert repo.rows[1]["status"] == 1
|
||
assert repo.rows[1]["update_user_name"] == STAMP_SELECTION
|
||
assert "LOGIN_FAILED" in (repo.rows[1]["fail_reason"] or "")
|