from __future__ import annotations from persist import build_result 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;EXPIRY_LT_12M;NOT_SELF_WAREHOUSE" assert result.allocations == [] # 毛利和效期同时命中时,原因里两句都在,不再只留优先级更高的一条。 def test_margin_and_expiry_both_in_reason(): result = run_selection( _purchase(), [ MallOutcome( "1号药城", candidates=[ _candidate( sku_id="MARGIN", vendor_raw=HANGZHOU, price=99.0, stock=100.0, ), _candidate( sku_id="EXP", vendor_raw=GUANGDONG, price=90.0, expiry_raw="6", expiry_months=6.0, stock=100.0, ), ], ) ], ) built = build_result(_purchase(), result) assert "毛利不大于 4%" in built.deal_result assert "效期不足 12 个月" in built.deal_result