from __future__ import annotations from decimal import Decimal from persist import build_result from pipeline import KIND_BUSINESS_FAIL, KIND_SUCCESS, KIND_SYSTEM_FAIL, SelectionResult from rules import Allocation, Candidate, PurchaseLine, Rejected, normalize_vendor HANGZHOU = "杭州小药药自营旗舰店" CHANGSHA = "长沙小药药自营旗舰店" GUANGDONG = "广东壹号药业有限公司" def _purchase(**overrides) -> PurchaseLine: data = { "approval_no": "国药准字H20100001", "sale_price": 100.0, "qty": 10.0, "oms_id": 2026100201, "bar_code": "06901234567890", "generic_name": "阿莫西林", "org_code": "907", } data.update(overrides) return PurchaseLine(**data) def _candidate(**overrides) -> Candidate: vendor_raw = overrides.get("vendor_raw", GUANGDONG) data = { "mall_name": "1号药城", "sku_id": "S1", "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": 20.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 _success(allocations: list[Allocation], malls: list[str] | None = None) -> SelectionResult: return SelectionResult( kind=KIND_SUCCESS, reason="SELECTED", allocations=allocations, rejected=[], malls=malls or ["1号药城", "药帮忙"], ) def test_split_two_platforms_writes_allocated_qty(): result = build_result( _purchase(), _success( [ Allocation( _candidate( mall_name="药帮忙", vendor_raw=HANGZHOU, price=88.0, ), 3, ), Allocation(_candidate(price=90.0), 7), ] ), ) assert result.deal_flag == 1 assert "选品成功" in result.deal_result assert len(result.cart_rows) == 2 ybm = next(row for row in result.cart_rows if row["platform_type"] == 48) yaoex = next(row for row in result.cart_rows if row["platform_type"] == 66) assert ybm["purchase_qty"] == 3 assert ybm["purchase_price"] == Decimal("88.0000") assert ybm["purchase_total"] == Decimal("264.0000") assert ybm["supplier_name"] == HANGZHOU assert yaoex["purchase_qty"] == 7 assert yaoex["purchase_total"] == Decimal("630.0000") assert all(row["status"] == 1 for row in result.cart_rows) assert all(row["multi_platform_flag"] == 1 for row in result.cart_rows) assert all(row["push_status"] == 0 for row in result.cart_rows) def test_two_warehouses_same_platform_flag_off(): result = build_result( _purchase(), _success( [ Allocation( _candidate(mall_name="药帮忙", vendor_raw=HANGZHOU, price=88.0), 3, ), Allocation( _candidate(mall_name="药帮忙", vendor_raw=CHANGSHA, price=90.0), 7, ), ], malls=["药帮忙"], ), ) assert len(result.cart_rows) == 2 assert all(row["platform_type"] == 48 for row in result.cart_rows) assert all(row["multi_platform_flag"] == 0 for row in result.cart_rows) def test_stock_insufficient_business_fail(): result = build_result( _purchase(), SelectionResult( kind=KIND_BUSINESS_FAIL, reason="STOCK_INSUFFICIENT", allocations=[], rejected=[Rejected(_candidate(vendor_raw="路边店"), "NOT_SELF_WAREHOUSE")], malls=["1号药城", "药帮忙"], ), ) assert result.deal_flag == 2 assert "库存不足" in result.deal_result assert len(result.cart_rows) == 2 assert all(row["status"] == 2 for row in result.cart_rows) assert all("库存不足" in (row["fail_reason"] or "") for row in result.cart_rows) assert all(row["multi_platform_flag"] == 1 for row in result.cart_rows) def test_margin_fail_text_is_4_percent(): result = build_result( _purchase(), SelectionResult( kind=KIND_BUSINESS_FAIL, reason="MARGIN_LT_4PCT", allocations=[], rejected=[], malls=["1号药城"], ), ) assert "毛利不足 4%" in result.deal_result assert "毛利不足 4%" in (result.cart_rows[0]["fail_reason"] or "") def test_system_fault_deal_result_fits_varchar2_200_byte(): blob = "药帮忙接口返回非JSON" + ("probe" * 40) result = build_result( _purchase(), SelectionResult( kind=KIND_SYSTEM_FAIL, reason="SYSTEM_FAULT", allocations=[], rejected=[], malls=["1号药城", "药帮忙"], mall_errors={"药帮忙": f"QUERY_FAILED:{blob}"}, ), ) assert len(result.deal_result.encode("gbk")) <= 200 assert result.deal_result.startswith("系统故障") def test_system_fault_uses_per_mall_errors(): result = build_result( _purchase(), SelectionResult( kind=KIND_SYSTEM_FAIL, reason="SYSTEM_FAULT", allocations=[], rejected=[], malls=["1号药城", "药帮忙"], mall_errors={"药帮忙": "LOGIN_FAILED:timeout"}, ), ) assert result.deal_flag == 2 assert result.deal_result.startswith("系统故障") ybm = next(row for row in result.cart_rows if row["platform_type"] == 48) yaoex = next(row for row in result.cart_rows if row["platform_type"] == 66) assert "LOGIN_FAILED:timeout" in (ybm["fail_reason"] or "") assert "对端平台不可用" in (yaoex["fail_reason"] or "") def test_failure_total_is_price_times_plan_qty(): result = build_result( _purchase(sale_price=12.5, qty=3), SelectionResult( kind=KIND_BUSINESS_FAIL, reason="NO_HIT", allocations=[], rejected=[], malls=["1号药城"], ), ) row = result.cart_rows[0] assert row["purchase_price"] == Decimal("12.5000") assert row["purchase_qty"] == 3 assert row["purchase_total"] == Decimal("37.5000") assert row["supplier_name"] == "无符合规则供应商"