強化 MOMO PChome 比價可信鏈路
All checks were successful
CD Pipeline / deploy (push) Successful in 1m45s

This commit is contained in:
OoO
2026-05-19 15:26:10 +08:00
parent ec378258e7
commit dafde7e1a7
32 changed files with 1794 additions and 117 deletions

View File

@@ -62,7 +62,7 @@ def test_fetch_candidates_falls_back_without_sales_on_sqlite_snapshot():
"competitor_product_id, competitor_product_name, tags, crawled_at"
") VALUES ("
"'SKU-1', 'pchome', NULL, 0.9, 999, 1200, 16.75, "
"'PCH-1', '測試商品 PChome', '[\"on_sale\"]', '2026-05-13 10:00:00'"
"'PCH-1', '測試商品 PChome', '[\"identity_v2\", \"on_sale\"]', '2026-05-13 10:00:00'"
")"
))

View File

@@ -19,7 +19,11 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
assert 'attempt_status="no_result"' in source
assert 'attempt_status="no_match"' in source
assert 'attempt_status="error"' in source
assert "_search_pchome_candidates(crawler, momo_name, search_terms)" in source
assert "_search_pchome_candidates(crawler, momo_name, search_terms, momo_price=momo_price)" in source
assert 'attempt_status="needs_review"' in source
assert "_should_upsert_competitor_price" in source
assert "replace_legacy_unverified" in source
assert "identity_v2" in source
assert "CREATE TABLE IF NOT EXISTS competitor_match_attempts" in migration
assert "attempt_status" in migration
@@ -31,12 +35,12 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes():
def test_competitor_feeder_logs_keyword_parser_fallback(monkeypatch, caplog):
from services import competitor_price_feeder
from services.price_comparison import ProductNameParser
from services import marketplace_product_matcher
def broken_parse(self, *_args, **_kwargs):
raise RuntimeError("parser unavailable")
def broken_build_search_terms(*_args, **_kwargs):
raise RuntimeError("matcher unavailable")
monkeypatch.setattr(ProductNameParser, "parse", broken_parse)
monkeypatch.setattr(marketplace_product_matcher, "build_search_terms", broken_build_search_terms)
caplog.set_level(logging.DEBUG, logger="services.competitor_price_feeder")
terms = competitor_price_feeder._build_search_keywords("理膚寶水 B5 修復霜 40ml")

View File

@@ -322,7 +322,10 @@ def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
assert "competitor.product_url" in dashboard
assert "dashboard-competition-badge" in dashboard
assert "decision.summary" in dashboard
assert "PChome 待比對" in dashboard
assert "PChome {{ match_status.label" in dashboard
assert "_load_pchome_match_attempt_map" in route_source
assert "低信心待審" in route_source
assert "規格衝突待審" in route_source
assert "series.pchome" in dashboard
assert "label: 'PChome'" in dashboard
assert "含 PChome 歷史快照" in dashboard
@@ -348,7 +351,9 @@ def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")
template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")
assert "MIN_MATCH_SCORE = 0.42" in feeder_source
assert "MIN_MATCH_SCORE = 0.62" in feeder_source
assert "REPLACE_DIFFERENT_PRODUCT_SCORE" in feeder_source
assert "marketplace_product_matcher" in feeder_source
assert "MAX_SEARCH_TERMS" in feeder_source
assert "_build_search_keywords" in feeder_source
assert "_search_pchome_candidates" in feeder_source

View File

@@ -0,0 +1,87 @@
from sqlalchemy import create_engine, text
def test_marketplace_matcher_accepts_same_product_identity():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"理膚寶水 B5 修復霜 40ml",
"理膚寶水 全面修復霜 B5 40ml",
momo_price=699,
competitor_price=679,
)
assert diagnostics.score >= 0.9
assert diagnostics.hard_veto is False
assert "brand_match" in diagnostics.tags
assert "spec_match" in diagnostics.tags
def test_marketplace_matcher_rejects_brand_conflict_even_when_volume_matches():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【蘭蔻】官方直營 玫瑰霜60ml+玫瑰精露150ml",
"LOREAL Paris 巴黎萊雅 金致臻顏花蜜奢養膠原輕盈乳霜_60ml",
momo_price=18765,
competitor_price=1249,
)
assert diagnostics.score < 0.62
assert diagnostics.hard_veto is True
assert "brand_conflict" in diagnostics.reasons
def test_marketplace_matcher_rejects_volume_conflict_for_same_brand():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【CLARINS 克蘭詩】黃金亮眼萃20mlX2囤貨組",
"【CLARINS克蘭詩】黃金雙萃精華 75ml",
momo_price=4500,
competitor_price=3549,
)
assert diagnostics.score < 0.62
assert diagnostics.hard_veto is True
assert "volume_conflict" in diagnostics.reasons
def test_marketplace_matcher_handles_bundle_piece_count():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【Relove】馬甲纖纖飲-莓果風味X3盒 共72包",
"【3入超值組】Relove 馬甲纖纖飲 24包/7克",
momo_price=4590,
competitor_price=4590,
)
assert diagnostics.score >= 0.62
assert diagnostics.hard_veto is False
def test_batch_compare_top_uses_latest_momo_price_not_revenue(monkeypatch):
from services import pchome_crawler
engine = create_engine("sqlite:///:memory:")
with engine.begin() as conn:
conn.execute(text("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, i_code TEXT)"))
conn.execute(text("CREATE TABLE daily_sales (product_id INTEGER, revenue NUMERIC, date TEXT)"))
conn.execute(text("CREATE TABLE price_records (id INTEGER PRIMARY KEY, product_id INTEGER, price NUMERIC, timestamp TEXT)"))
conn.execute(text("INSERT INTO products VALUES (1, '理膚寶水 B5 修復霜 40ml', 'SKU001')"))
conn.execute(text("INSERT INTO daily_sales VALUES (1, 999999, '2026-05-19')"))
conn.execute(text("INSERT INTO price_records VALUES (1, 1, 699, '2026-05-19 10:00:00')"))
calls = []
def fake_compare_product(name, momo_price, momo_icode=""):
calls.append((name, momo_price, momo_icode))
return {"found": True, "momo_price": momo_price}
monkeypatch.setattr(pchome_crawler, "compare_product", fake_compare_product)
results = pchome_crawler.batch_compare_top(engine, top_n=1)
assert results == [{"found": True, "momo_price": 699.0}]
assert calls == [("理膚寶水 B5 修復霜 40ml", 699.0, "SKU001")]