from cart_job import KIND_BUSINESS, KIND_OK, KIND_SYSTEM, AddOutcome, run_cart_batch from cart_match import CartOffer from cart_repo import STAMP_DONE, STAMP_SELECTION, MemoryCartRepo HANGZHOU = "杭州小药药自营旗舰店" GUANGDONG = "广东壹号药业有限公司" YBM = 48 YAOEX = 66 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) 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) 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 def test_job_marks_out_of_stock(): repo = MemoryCartRepo([_row(purchase_qty=10)]) 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 def test_job_marks_login_fail_as_system_error(): 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"] == 2 assert repo.rows[1]["fail_reason"].startswith("系统故障:") assert "账号密码错误" in repo.rows[1]["fail_reason"] assert repo.rows[2]["status"] == 1 assert repo.rows[2]["update_user_name"] == STAMP_DONE assert ybm_added == [3] 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 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_marks_pack_reject(): repo = MemoryCartRepo([_row(purchase_qty=7)]) def search_yaoex(bar_code: str): return [_offer(GUANGDONG, extra={"raw": {"middlePackNum": 10}})] 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"].startswith("中包装加不进") 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(): repo = MemoryCartRepo([_row()]) def search_yaoex(bar_code: str): raise RuntimeError("网关 502") 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]["fail_reason"].startswith("系统故障:") assert "网关 502" in repo.rows[1]["fail_reason"]