186 lines
5.4 KiB
Python
186 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
from pipeline import MallOutcome, run_selection
|
|
from rules import Candidate, PurchaseLine, normalize_vendor
|
|
|
|
|
|
HANGZHOU = "杭州小药药自营旗舰店"
|
|
GUANGDONG = "广东壹号药业有限公司"
|
|
|
|
|
|
def _purchase(**overrides) -> PurchaseLine:
|
|
data = {
|
|
"approval_no": "国药准字H20100001",
|
|
"sale_price": 100.0,
|
|
"qty": 10.0,
|
|
"row_no": 2,
|
|
"generic_name": "阿莫西林",
|
|
"oms_id": 2026100201,
|
|
"bar_code": "06901234567890",
|
|
}
|
|
data.update(overrides)
|
|
return PurchaseLine(**data)
|
|
|
|
|
|
def _candidate(**overrides) -> Candidate:
|
|
vendor_raw = overrides.get("vendor_raw", GUANGDONG)
|
|
data = {
|
|
"mall_name": "1号药城",
|
|
"sku_id": "SKU1",
|
|
"name": "测试药品",
|
|
"spec": "0.25g",
|
|
"price": 90.0,
|
|
"expiry_raw": "13",
|
|
"expiry_months": 13.0,
|
|
"vendor_raw": vendor_raw,
|
|
"vendor_norm": normalize_vendor(vendor_raw),
|
|
"stock": 100.0,
|
|
"extra": {},
|
|
}
|
|
data.update(overrides)
|
|
if "vendor_raw" in overrides and "vendor_norm" not in overrides:
|
|
data["vendor_norm"] = normalize_vendor(overrides["vendor_raw"])
|
|
return Candidate(**data)
|
|
|
|
|
|
def test_missing_sale_price_is_business_fail():
|
|
result = run_selection(
|
|
_purchase(sale_price=None),
|
|
[MallOutcome("1号药城", candidates=[_candidate()])],
|
|
)
|
|
assert result.kind == "BUSINESS_FAIL"
|
|
assert result.reason == "NO_SALE_PRICE"
|
|
assert result.allocations == []
|
|
|
|
|
|
def test_missing_qty_is_business_fail():
|
|
result = run_selection(_purchase(qty=0), [MallOutcome("药帮忙")])
|
|
assert result.kind == "BUSINESS_FAIL"
|
|
assert result.reason == "NO_QTY"
|
|
|
|
|
|
def test_query_error_without_enough_stock_is_system_fail():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome("1号药城", failed_reason="QUERY_FAILED:timeout"),
|
|
MallOutcome("药帮忙", candidates=[]),
|
|
],
|
|
)
|
|
assert result.kind == "SYSTEM_FAIL"
|
|
assert result.allocations == []
|
|
assert result.mall_errors["1号药城"] == "QUERY_FAILED:timeout"
|
|
|
|
|
|
def test_empty_hit_is_no_hit():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome("1号药城", candidates=[]),
|
|
MallOutcome("药帮忙", candidates=[]),
|
|
],
|
|
)
|
|
assert result.kind == "BUSINESS_FAIL"
|
|
assert result.reason == "NO_HIT"
|
|
|
|
|
|
def test_all_rejected_uses_rule_reason():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[MallOutcome("1号药城", candidates=[_candidate(vendor_raw="路边药店")])],
|
|
)
|
|
assert result.kind == "BUSINESS_FAIL"
|
|
assert result.reason == "NOT_SELF_WAREHOUSE"
|
|
assert any(item.reason == "NOT_SELF_WAREHOUSE" for item in result.rejected)
|
|
|
|
|
|
def test_split_across_malls_is_success():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome(
|
|
"药帮忙",
|
|
candidates=[
|
|
_candidate(
|
|
sku_id="HZ",
|
|
mall_name="药帮忙",
|
|
vendor_raw=HANGZHOU,
|
|
stock=3.0,
|
|
)
|
|
],
|
|
),
|
|
MallOutcome(
|
|
"1号药城",
|
|
candidates=[_candidate(sku_id="GD", stock=20.0)],
|
|
),
|
|
],
|
|
)
|
|
assert result.kind == "SUCCESS"
|
|
assert result.reason == "SELECTED"
|
|
qtys = [(item.candidate.sku_id, item.qty) for item in result.allocations]
|
|
assert qtys == [("HZ", 3), ("GD", 7)]
|
|
|
|
|
|
def test_one_mall_login_failed_other_fills_is_success():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome("药帮忙", failed_reason="LOGIN_FAILED:timeout"),
|
|
MallOutcome("1号药城", candidates=[_candidate(stock=20.0)]),
|
|
],
|
|
)
|
|
assert result.kind == "SUCCESS"
|
|
assert result.allocations[0].qty == 10
|
|
|
|
|
|
def test_one_mall_login_failed_other_short_is_system_fail():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome("药帮忙", failed_reason="LOGIN_FAILED:timeout"),
|
|
MallOutcome("1号药城", candidates=[_candidate(stock=4.0)]),
|
|
],
|
|
)
|
|
assert result.kind == "SYSTEM_FAIL"
|
|
assert result.allocations == []
|
|
assert result.mall_errors["药帮忙"].startswith("LOGIN_FAILED")
|
|
|
|
|
|
def test_short_stock_with_rejects_is_stock_insufficient():
|
|
result = run_selection(
|
|
_purchase(),
|
|
[
|
|
MallOutcome(
|
|
"药帮忙",
|
|
candidates=[
|
|
_candidate(
|
|
sku_id="HZ",
|
|
mall_name="药帮忙",
|
|
vendor_raw=HANGZHOU,
|
|
stock=4.0,
|
|
),
|
|
_candidate(
|
|
sku_id="ROAD",
|
|
mall_name="药帮忙",
|
|
vendor_raw="路边药店",
|
|
stock=100.0,
|
|
),
|
|
],
|
|
),
|
|
MallOutcome(
|
|
"1号药城",
|
|
candidates=[
|
|
_candidate(
|
|
sku_id="EXP",
|
|
expiry_raw="6",
|
|
expiry_months=6.0,
|
|
stock=100.0,
|
|
)
|
|
],
|
|
),
|
|
],
|
|
)
|
|
assert result.kind == "BUSINESS_FAIL"
|
|
assert result.reason == "STOCK_INSUFFICIENT"
|
|
assert result.allocations == []
|