[V10.350] add retryable identity recovery loop
This commit is contained in:
@@ -618,6 +618,124 @@ def test_competitor_feeder_refreshes_expired_identity_by_known_product_id(monkey
|
||||
assert attempts[0]["search_terms"] == ["known_product_id:DDAB01-1900ABCD"]
|
||||
|
||||
|
||||
def test_competitor_feeder_refresh_recovers_with_fresh_search_when_known_id_is_low_score(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
stale = PChomeProduct(
|
||||
product_id="DDAB01-STALE",
|
||||
name="Panasonic 國際牌 男仕防水美體除毛器 國際版 (ER-GK83)",
|
||||
price=2290,
|
||||
original_price=2490,
|
||||
discount=8,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-STALE",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.7,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
recovered = PChomeProduct(
|
||||
product_id="DDAB01-RECOVERED",
|
||||
name="Panasonic 國際牌 男士身體除毛器 ER-GK83",
|
||||
price=2390,
|
||||
original_price=2490,
|
||||
discount=4,
|
||||
image_url="",
|
||||
product_url="https://24h.pchome.com.tw/prod/DDAB01-RECOVERED",
|
||||
stock=20,
|
||||
store="24h",
|
||||
rating=4.8,
|
||||
review_count=8,
|
||||
is_on_sale=True,
|
||||
crawled_at=datetime.now(),
|
||||
)
|
||||
|
||||
class FakeCrawler:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def fetch_product_details(self, product_ids, batch_size=20):
|
||||
assert product_ids == ["DDAB01-STALE"]
|
||||
return True, "ok", [stale]
|
||||
|
||||
def search_products(self, *_args, **_kwargs):
|
||||
return True, "ok", [stale, recovered]
|
||||
|
||||
def fake_score(_momo_name, competitor_name, **_kwargs):
|
||||
if "RECOVERED" in competitor_name or "男士身體除毛器" in competitor_name:
|
||||
return SimpleNamespace(
|
||||
score=0.81,
|
||||
brand_score=1.0,
|
||||
token_score=0.8,
|
||||
spec_score=0.8,
|
||||
sequence_score=0.72,
|
||||
type_score=0.55,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=("shared_model_token",),
|
||||
comparison_mode="exact_identity",
|
||||
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
|
||||
)
|
||||
return SimpleNamespace(
|
||||
score=0.68,
|
||||
brand_score=1.0,
|
||||
token_score=0.55,
|
||||
spec_score=0.55,
|
||||
sequence_score=0.6,
|
||||
type_score=0.55,
|
||||
price_penalty=0.0,
|
||||
hard_veto=False,
|
||||
reasons=(),
|
||||
comparison_mode="exact_identity",
|
||||
tags=["identity_v2", "comparison_exact_identity", "brand_match"],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler)
|
||||
monkeypatch.setattr("services.marketplace_product_matcher.score_marketplace_match", fake_score)
|
||||
feeder = CompetitorPriceFeeder(engine=object())
|
||||
attempts = []
|
||||
writes = []
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_should_upsert_competitor_price",
|
||||
lambda *_args, **_kwargs: (True, "same_or_empty_existing"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_upsert_competitor_price",
|
||||
lambda sku, product, score, tags, **kwargs: writes.append({
|
||||
"sku": sku,
|
||||
"product_id": product.product_id,
|
||||
"score": score,
|
||||
"tags": tags,
|
||||
**kwargs,
|
||||
}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
feeder,
|
||||
"_record_match_attempt",
|
||||
lambda *args, **kwargs: attempts.append(kwargs),
|
||||
)
|
||||
|
||||
result = feeder._run_known_identity_refresh_items([{
|
||||
"sku": "TP00090100000153",
|
||||
"name": "【Panasonic 國際牌】男士身體除毛器 2025新款 ER-GK83 日版 日本直送",
|
||||
"product_id": 1,
|
||||
"momo_price": 2490,
|
||||
"competitor_product_id": "DDAB01-STALE",
|
||||
}])
|
||||
|
||||
assert result.matched == 1
|
||||
assert writes[0]["product_id"] == "DDAB01-RECOVERED"
|
||||
assert "fresh_search_recovery" in writes[0]["tags"]
|
||||
assert attempts[0]["attempt_status"] == "matched"
|
||||
assert "known_product_id:DDAB01-STALE" in attempts[0]["search_terms"]
|
||||
assert any("Panasonic" in term or "國際牌" in term for term in attempts[0]["search_terms"])
|
||||
|
||||
|
||||
def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch):
|
||||
from services.competitor_price_feeder import CompetitorPriceFeeder
|
||||
from services.pchome_crawler import PChomeProduct
|
||||
|
||||
@@ -214,6 +214,50 @@ def test_marketplace_matcher_rejects_product_type_conflict_even_when_line_matche
|
||||
assert diagnostics.comparison_mode == "not_comparable"
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_foundation_stick_vs_foundation_liquid():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【蘭蔻】零粉感超持久粉底棒9.5g",
|
||||
"【LANCOME 蘭蔻】零粉感超持久粉底 30ml",
|
||||
momo_price=1620,
|
||||
competitor_price=1580,
|
||||
)
|
||||
|
||||
assert diagnostics.score < 0.76
|
||||
assert diagnostics.hard_veto is True
|
||||
assert "type_conflict" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_nivea_deodorant_spray_identity():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【NIVEA 妮維雅】男士 止汗爽身噴霧 無印乾爽-清新海洋",
|
||||
"NIVEA 妮維雅 男士止汗爽身噴霧 無印乾爽-清新海洋150ml",
|
||||
momo_price=159,
|
||||
competitor_price=169,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
|
||||
|
||||
def test_marketplace_matcher_promotes_packaging_variant_for_same_nars_powder():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
diagnostics = score_marketplace_match(
|
||||
"【NARS】官方直營 裸光蜜粉餅(璀璨奢金限定版/星沙金小白餅)",
|
||||
"【NARS】裸光蜜粉餅(小白餅) 10g",
|
||||
momo_price=1050,
|
||||
competitor_price=1050,
|
||||
)
|
||||
|
||||
assert diagnostics.score >= 0.76
|
||||
assert diagnostics.hard_veto is False
|
||||
assert "shared_identity_anchor_packaging_variant" in diagnostics.reasons
|
||||
|
||||
|
||||
def test_marketplace_matcher_rejects_same_count_different_unit_family():
|
||||
from services.marketplace_product_matcher import score_marketplace_match
|
||||
|
||||
|
||||
Reference in New Issue
Block a user