From 124ddc7376d38a0c75fd8a23e0dd87d00646bd75 Mon Sep 17 00:00:00 2001 From: OoO Date: Wed, 20 May 2026 00:05:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20PChome=20=E5=96=AE?= =?UTF-8?q?=E4=BD=8D=E5=83=B9=E6=AF=94=E8=BC=83=E8=AD=89=E6=93=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 2 +- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 6 +- routes/dashboard_routes.py | 26 +++- services/competitor_intel_repository.py | 28 +++- services/competitor_price_feeder.py | 1 + services/marketplace_product_matcher.py | 124 ++++++++++++++++++ services/ppt_generator.py | 7 +- tests/test_competitor_identity_revalidator.py | 6 +- tests/test_competitor_intel_cache.py | 2 + ...t_competitor_match_attempts_persistence.py | 1 + tests/test_marketplace_product_matcher.py | 27 ++++ 12 files changed, 223 insertions(+), 9 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 6374a5b..2049dc3 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,7 +4,7 @@ ================================================================================ 【已完成】 - - V10.292 補核心 MOMO/PChome 比價第三層語意:同核心商品但買送、套組、件數不同且只有單一基礎規格時標記 `unit_comparable`,只寫入 `competitor_match_attempts`,商品看板顯示「需單位價比較」;多容量/多品項套組仍保持不可比較,避免把不同販售組合直接寫進正式總價差。 + - V10.293 補核心 MOMO/PChome 比價第三層語意:同核心商品但買送、套組、件數不同且只有單一基礎規格時標記 `unit_comparable`,只寫入 `competitor_match_attempts`,商品看板顯示「需單位價比較」與單位價換算;多容量/多品項套組仍保持不可比較,避免把不同販售組合直接寫進正式總價差。 - V10.289 重排 Elephant Alpha L3 HITL `ea_escalation` Telegram 告警:改成專業 incident brief 格式,分成決策狀態、背景摘要、風險摘要、TOP 待審 SKU 與建議處置;價格行動會拆出 MOMO/PChome 價格、價差、人工處置與 PChome ID,避免長 bullet 難讀。 - V10.284 關閉 Code Review Hermes LLM scan 預設路徑:Step 2 改 deterministic fast static scan,不再讓部署後先卡三段 Ollama timeout;若需要 LLM scan 可用 `CODE_REVIEW_HERMES_LLM_SCAN_ENABLED=true` 顯式開啟,仍只走本地矩陣、不走 Gemini。 - V10.283 將 Code Review Hermes scan 收斂為 fast compact prompt:預設 2 檔 × 900 字、輸出 384 tokens,仍走 GCP-A → GCP-B → 111 本地矩陣,避免部署後 code_review_hermes 先卡三段 timeout。 diff --git a/config.py b/config.py index 9aca5f7..27d4e25 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.292" +SYSTEM_VERSION = "V10.293" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index dec7520..bf9ef93 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-19 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 僅備援 / 鎖定場景 -> **適用版本**: V10.292 +> **適用版本**: V10.293 --- @@ -308,7 +308,7 @@ LIMIT 300 | 比對算法 | 品牌 + 核心 token + 容量/重量/包數 + 品類 + 價格 sanity check | 由 `marketplace_product_matcher.py` 統一供 feeder、legacy crawler、AI/PPT 鏈路使用 | | 最低比對門檻 | 0.76 | 核心比價寧可待審,不允許低信心錯配影響 AI 決策 | | 已有不同 PChome 商品覆蓋門檻 | 0.84 | 新候選與既有正式配對不同時,除非超高信心,否則寫入 `needs_review` attempt 不覆蓋 | -| 單位價可比模式 | `unit_comparable` | 同核心商品但買送/套組/件數不同時,不寫正式總價差;只寫入 attempt,供單位價或人工覆核 | +| 單位價可比模式 | `unit_comparable` | 同核心商品但買送/套組/件數不同時,不寫正式總價差;只寫入 attempt,並以單位價證據供 Dashboard / PPT / AI 報表與人工覆核 | | 語意標籤 | JSONB 陣列 | 傳給 Hermes 提升情境感知品質 | ### 競品比對邏輯(`competitor_price_feeder.py`) @@ -345,7 +345,7 @@ LEFT JOIN competitor_prices cp - `services/competitor_identity_revalidator.py` 可對既有 `competitor_prices` legacy row 離線重跑 `identity_v2`:只有新版 matcher 分數 `>= 0.76` 且無 hard veto 才補 `identity_v2` / `legacy_revalidated` tags;預設不刷新 `expires_at`,避免過期價格進入決策。 - `CompetitorPriceFeeder.run_expired_identity_refresh()` 會優先刷新已通過 `identity_v2` 但 TTL 過期的 PChome row:直接用既有 `competitor_product_id` 批次呼叫 PChome 商品 API,再用新版 matcher 重新驗證名稱/規格/價格 sanity,通過後寫回 `competitor_prices` 與 `competitor_price_history`。這條路徑提升新鮮價格覆蓋率,但不降低 match threshold,也不讓過期價格直接進入決策。 - `marketplace_product_matcher.py` 的擴充只能走「正向證據 + 反向 veto」:品牌一致、商品線/型號訊號強、價格合理且無 hard veto 時才允許 `strong_product_line_match` 加分;補充瓶/補充包/refill 與一般正裝不互相配對,分享組/加量組/明星組等組合包不得誤配單品。 -- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時,matcher 必須回傳 `comparison_mode='unit_comparable'` 與 `unit_comparable` reason;Feeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'` 或 `refresh_unit_comparable`,不得寫入 `competitor_prices`,直到下游支援單位價換算或人工覆核。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`。 +- 套組/買送/件數不同但品牌、核心商品線與單一基礎規格一致時,matcher 必須回傳 `comparison_mode='unit_comparable'` 與 `unit_comparable` reason;Feeder 只能寫入 `competitor_match_attempts.attempt_status='unit_comparable'` 或 `refresh_unit_comparable`,不得寫入 `competitor_prices`。Dashboard 與 `competitor_intel_repository` 必須用 `build_unit_price_comparison()` 產生每 ml / 每 g / 每入單位價證據,讓 PPT / AI 報表可說明「需單位價比較」而不是把總價當同款價差。若任一側含多個不同容量/重量規格,視為多品項套組,不可進 `unit_comparable`。 - PChome feeder 的外部 request timeout 由 `PCHOME_FEEDER_TIMEOUT` 控制,預設 12 秒;排程不得因單一 PChome 搜尋 API timeout 被拖到數分鐘。 - 商品看板的 PChome 狀態必須把 matcher 診斷原因翻成可行動語意:品牌衝突、規格衝突、補充包差異、組合差異、商品線不符等,不可只顯示籠統「待比對」或「身份否決」。 - Dashboard 必須把「待比對」拆成可診斷狀態:`價格過期待刷新`、`舊版配對待重驗`、`低分配對待審`、`身份否決`、`需單位價比較`、`找不到同款`、`抓取異常`、`尚未搜尋`。不可再用單一「待比對」掩蓋資料品質原因。 diff --git a/routes/dashboard_routes.py b/routes/dashboard_routes.py index 20825a0..bf88a22 100644 --- a/routes/dashboard_routes.py +++ b/routes/dashboard_routes.py @@ -120,6 +120,14 @@ def _build_pchome_match_status(attempt=None, ineligible=None): if status in {'unit_comparable', 'refresh_unit_comparable'}: score = _to_float(attempt.get('best_match_score')) score_text = f"最佳候選 {round(score * 100)}%" if score is not None else "已找到同核心候選" + unit_comparison = attempt.get('unit_comparison') or {} + if unit_comparison.get('comparable'): + return { + 'label': '需單位價比較', + 'tone': 'watch', + 'summary': f"已換算單位價:{unit_comparison.get('summary')};仍需人工確認檔期與贈品條件", + 'detail': score_text, + } return { 'label': '需單位價比較', 'tone': 'watch', @@ -423,7 +431,23 @@ def _load_pchome_match_attempt_map(session, skus): sys_log.warning(f"[Dashboard] PChome 比對嘗試資料讀取略過: {exc}") return {} - return {str(row.get('sku')): dict(row) for row in rows} + result = {} + for row in rows: + item = dict(row) + if item.get('attempt_status') in {'unit_comparable', 'refresh_unit_comparable'}: + try: + from services.marketplace_product_matcher import build_unit_price_comparison + item['unit_comparison'] = build_unit_price_comparison( + item.get('momo_product_name') or '', + item.get('best_competitor_product_name') or '', + item.get('momo_price'), + item.get('best_competitor_price'), + ) + except Exception as exc: + sys_log.warning(f"[Dashboard] PChome 單位價比較資料建立略過: {exc}") + item['unit_comparison'] = {'comparable': False, 'reason': 'build_error'} + result[str(row.get('sku'))] = item + return result def _format_dashboard_dt(value): diff --git a/services/competitor_intel_repository.py b/services/competitor_intel_repository.py index 7ea9769..89cf6f5 100644 --- a/services/competitor_intel_repository.py +++ b/services/competitor_intel_repository.py @@ -421,6 +421,9 @@ def fetch_competitor_comparison_results( NULL AS sku, NULL AS attempt_status, NULL AS candidate_count, + NULL AS best_competitor_product_id, + NULL AS best_competitor_product_name, + NULL AS best_competitor_price, NULL AS best_match_score, NULL AS error_message, NULL AS attempted_at @@ -468,6 +471,9 @@ def fetch_competitor_comparison_results( cma.sku, cma.attempt_status, cma.candidate_count, + cma.best_competitor_product_id, + cma.best_competitor_product_name, + cma.best_competitor_price, cma.best_match_score, cma.error_message, cma.attempted_at @@ -517,6 +523,9 @@ def fetch_competitor_comparison_results( vc.match_score, la.attempt_status, la.candidate_count, + la.best_competitor_product_id, + la.best_competitor_product_name, + la.best_competitor_price, la.best_match_score, la.error_message, la.attempted_at, @@ -539,6 +548,19 @@ def fetch_competitor_comparison_results( for row in rows: pchome_id = row.get("competitor_product_id") found = bool(row.get("pchome_price")) + match_status = "matched" if found else (row.get("attempt_status") or "no_valid_match") + unit_comparison = None + if match_status in {"unit_comparable", "refresh_unit_comparable"}: + try: + from services.marketplace_product_matcher import build_unit_price_comparison + unit_comparison = build_unit_price_comparison( + row.get("name") or "", + row.get("best_competitor_product_name") or "", + row.get("momo_price"), + row.get("best_competitor_price"), + ) + except Exception: + unit_comparison = {"comparable": False, "reason": "build_error"} results.append({ "found": found, "momo_icode": str(row.get("sku") or ""), @@ -547,14 +569,18 @@ def fetch_competitor_comparison_results( "pc_name": row.get("competitor_product_name") or "", "pc_price": _num(row.get("pchome_price")), "pc_url": f"https://24h.pchome.com.tw/prod/{pchome_id}" if pchome_id else "", + "candidate_pc_id": row.get("best_competitor_product_id"), + "candidate_pc_name": row.get("best_competitor_product_name") or "", + "candidate_pc_price": _num(row.get("best_competitor_price")), "price_diff": _num(row.get("price_diff")), "price_diff_pct": _num(row.get("price_diff_pct")), "match_score": _num(row.get("match_score")), "momo_revenue": _num(row.get("momo_revenue")), - "match_status": "matched" if found else (row.get("attempt_status") or "no_valid_match"), + "match_status": match_status, "candidate_count": int(row.get("candidate_count") or 0), "best_match_score": _num(row.get("best_match_score")), "match_diagnostic": row.get("error_message") or "", + "unit_comparison": unit_comparison, }) return results diff --git a/services/competitor_price_feeder.py b/services/competitor_price_feeder.py index af01f8b..92ea456 100644 --- a/services/competitor_price_feeder.py +++ b/services/competitor_price_feeder.py @@ -153,6 +153,7 @@ def _format_match_diagnostics(diagnostics) -> str: f"token={diagnostics.token_score}; spec={diagnostics.spec_score}; " f"seq={diagnostics.sequence_score}; type={diagnostics.type_score}; " f"penalty={diagnostics.price_penalty}; veto={diagnostics.hard_veto}; " + f"mode={getattr(diagnostics, 'comparison_mode', 'exact_identity')}; " f"reasons={reasons}" ) diff --git a/services/marketplace_product_matcher.py b/services/marketplace_product_matcher.py index 8415c37..3358e80 100644 --- a/services/marketplace_product_matcher.py +++ b/services/marketplace_product_matcher.py @@ -168,6 +168,34 @@ class MatchDiagnostics: return tags +@dataclass(frozen=True) +class UnitPriceComparison: + comparable: bool + reason: str + unit_label: str = "" + momo_total_quantity: Optional[float] = None + competitor_total_quantity: Optional[float] = None + momo_unit_price: Optional[float] = None + competitor_unit_price: Optional[float] = None + unit_gap_amount: Optional[float] = None + unit_gap_pct: Optional[float] = None + summary: str = "" + + def as_dict(self) -> dict: + return { + "comparable": self.comparable, + "reason": self.reason, + "unit_label": self.unit_label, + "momo_total_quantity": self.momo_total_quantity, + "competitor_total_quantity": self.competitor_total_quantity, + "momo_unit_price": self.momo_unit_price, + "competitor_unit_price": self.competitor_unit_price, + "unit_gap_amount": self.unit_gap_amount, + "unit_gap_pct": self.unit_gap_pct, + "summary": self.summary, + } + + def normalize_product_text(value: str) -> str: text = unicodedata.normalize("NFKC", value or "") text = "".join( @@ -482,6 +510,29 @@ def _spec_mention_count(identity: ProductIdentity) -> int: return len(re.findall(r"\d+(?:\.\d+)?\s*(?:ml|毫升|l|g|公克|kg)", identity.normalized_name, re.I)) +def _count_text_value(value: str) -> Optional[int]: + if value.isdigit(): + return int(value) + return CHINESE_COUNT.get(value) + + +def _pack_multiplier(identity: ProductIdentity) -> int: + text = identity.normalized_name + buy_get = re.search(r"買\s*(\d+|[一二兩雙三四五六七八九十])\s*送\s*(\d+|[一二兩雙三四五六七八九十])", text) + if buy_get: + left = _count_text_value(buy_get.group(1)) or 0 + right = _count_text_value(buy_get.group(2)) or 0 + if left + right > 1: + return left + right + if "買一送一" in text or "買1送1" in text: + return 2 + + multipliers = [count for count, unit in identity.counts if unit in COUNT_UNITS and count > 1] + if multipliers: + return max(multipliers) + return 1 + + def _has_overlapping_base_spec(left: ProductIdentity, right: ProductIdentity) -> bool: left_volumes = tuple(sorted(set(left.volumes_ml))) right_volumes = tuple(sorted(set(right.volumes_ml))) @@ -504,6 +555,79 @@ def _has_overlapping_base_spec(left: ProductIdentity, right: ProductIdentity) -> return False +def _single_unit_total(identity: ProductIdentity) -> tuple[Optional[str], Optional[float], str]: + volumes = tuple(sorted(set(identity.volumes_ml))) + weights = tuple(sorted(set(identity.weights_g))) + if volumes and weights: + return None, None, "mixed_volume_weight" + if len(volumes) > 1 or len(weights) > 1: + return None, None, "multi_spec_component" + if volumes: + return "ml", volumes[0] * _pack_multiplier(identity), "ok" + if weights: + multiplier = identity.total_piece_count or _pack_multiplier(identity) + return "g", weights[0] * multiplier, "ok" + if identity.total_piece_count: + return "入", float(identity.total_piece_count), "ok" + return None, None, "missing_single_unit" + + +def build_unit_price_comparison( + momo_name: str, + competitor_name: str, + momo_price: Optional[float], + competitor_price: Optional[float], +) -> dict: + """Build deterministic unit-price evidence for unit-comparable candidates.""" + diagnostics = score_marketplace_match( + momo_name, + competitor_name, + momo_price=momo_price, + competitor_price=competitor_price, + ) + if diagnostics.comparison_mode != "unit_comparable": + return UnitPriceComparison(False, diagnostics.comparison_mode).as_dict() + + left = parse_product_identity(momo_name) + right = parse_product_identity(competitor_name) + left_unit, left_total, left_reason = _single_unit_total(left) + right_unit, right_total, right_reason = _single_unit_total(right) + if left_reason != "ok" or right_reason != "ok": + return UnitPriceComparison(False, f"{left_reason}:{right_reason}").as_dict() + if left_unit != right_unit or not left_total or not right_total: + return UnitPriceComparison(False, "unit_mismatch").as_dict() + + try: + momo_price_num = float(momo_price or 0) + competitor_price_num = float(competitor_price or 0) + except (TypeError, ValueError): + return UnitPriceComparison(False, "invalid_price").as_dict() + if momo_price_num <= 0 or competitor_price_num <= 0: + return UnitPriceComparison(False, "invalid_price").as_dict() + + momo_unit_price = momo_price_num / left_total + competitor_unit_price = competitor_price_num / right_total + unit_gap_amount = momo_unit_price - competitor_unit_price + unit_gap_pct = unit_gap_amount / competitor_unit_price * 100 if competitor_unit_price else 0 + summary = ( + f"MOMO ${momo_unit_price:.2f}/{left_unit} vs " + f"PChome ${competitor_unit_price:.2f}/{left_unit} " + f"({unit_gap_pct:+.1f}%)" + ) + return UnitPriceComparison( + comparable=True, + reason="unit_comparable", + unit_label=left_unit, + momo_total_quantity=round(left_total, 3), + competitor_total_quantity=round(right_total, 3), + momo_unit_price=round(momo_unit_price, 4), + competitor_unit_price=round(competitor_unit_price, 4), + unit_gap_amount=round(unit_gap_amount, 4), + unit_gap_pct=round(unit_gap_pct, 2), + summary=summary, + ).as_dict() + + def _is_unit_comparable_candidate( left: ProductIdentity, right: ProductIdentity, diff --git a/services/ppt_generator.py b/services/ppt_generator.py index 8b642ec..b57ec97 100644 --- a/services/ppt_generator.py +++ b/services/ppt_generator.py @@ -2533,7 +2533,11 @@ def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> s pc_wins = [r for r in found if r.get("price_diff", 0) < -10] mo_wins = [r for r in found if r.get("price_diff", 0) > 10] tie = [r for r in found if abs(r.get("price_diff", 0)) <= 10] - not_found = [r for r in results if not r.get("found")] + unit_comparable = [ + r for r in results + if not r.get("found") and r.get("match_status") in ("unit_comparable", "refresh_unit_comparable") + ] + not_found = [r for r in results if not r.get("found") and r not in unit_comparable] total = len(results) match_rate = len(found) / total * 100 if total else 0 avg_pct = (sum(r.get("price_diff_pct", 0) for r in found) / len(found) @@ -2572,6 +2576,7 @@ def generate_competitor_ppt(period_label: str, db_data: dict, ai_text: str) -> s ("PChome 更便宜", len(pc_wins), total, _BAR_PCHOME), ("momo 更便宜", len(mo_wins), total, _BAR_MOMO), ("價格相近", len(tie), total, _BAR_TIE), + ("需單位價比較", len(unit_comparable), total, "9A6A2F"), ("未找到對應", len(not_found), total, _BAR_MISS), ] row_t = 6.4 diff --git a/tests/test_competitor_identity_revalidator.py b/tests/test_competitor_identity_revalidator.py index d848bcc..6a11f1d 100644 --- a/tests/test_competitor_identity_revalidator.py +++ b/tests/test_competitor_identity_revalidator.py @@ -92,8 +92,12 @@ def test_dashboard_match_status_explains_unit_comparable_bundle(): "attempt_status": "unit_comparable", "best_match_score": 0.74, "error_message": "score=0.74; reasons=bundle_offer_conflict,unit_comparable", + "unit_comparison": { + "comparable": True, + "summary": "MOMO $14.99/ml vs PChome $16.98/ml (-11.7%)", + }, }) assert status["label"] == "需單位價比較" assert status["tone"] == "watch" - assert "不可直接比總價" in status["summary"] + assert "已換算單位價" in status["summary"] diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py index af1bf01..6f788dc 100644 --- a/tests/test_competitor_intel_cache.py +++ b/tests/test_competitor_intel_cache.py @@ -46,6 +46,8 @@ def test_competitor_ppt_results_keep_pending_diagnostics_in_export(): assert "\"found\": found" in source assert "\"match_status\"" in source assert "\"candidate_count\"" in source + assert "\"unit_comparison\"" in source + assert "build_unit_price_comparison" in source assert "(vc.pchome_price IS NULL)" in source diff --git a/tests/test_competitor_match_attempts_persistence.py b/tests/test_competitor_match_attempts_persistence.py index 0bc9814..d4e9892 100644 --- a/tests/test_competitor_match_attempts_persistence.py +++ b/tests/test_competitor_match_attempts_persistence.py @@ -30,6 +30,7 @@ def test_competitor_feeder_persists_all_match_attempt_outcomes(): assert "refresh_known_identity" in source assert 'attempt_status="unit_comparable"' in source assert 'attempt_status="refresh_unit_comparable"' in source + assert "mode={getattr(diagnostics, 'comparison_mode'" in source assert 'PCHOME_FEEDER_TIMEOUT", "12"' in source assert "PChomeCrawler(timeout=REQUEST_TIMEOUT" in source diff --git a/tests/test_marketplace_product_matcher.py b/tests/test_marketplace_product_matcher.py index 048130d..7a01e79 100644 --- a/tests/test_marketplace_product_matcher.py +++ b/tests/test_marketplace_product_matcher.py @@ -109,6 +109,25 @@ def test_marketplace_matcher_marks_bundle_single_as_unit_comparable_not_exact(): assert "comparison_unit_comparable" in diagnostics.tags +def test_unit_price_comparison_builds_normalized_evidence(): + from services.marketplace_product_matcher import build_unit_price_comparison + + comparison = build_unit_price_comparison( + "理膚寶水 B5 全面修復霜 40ml x2 超值組", + "理膚寶水 全面修復霜 B5 40ml", + momo_price=1199, + competitor_price=679, + ) + + assert comparison["comparable"] is True + assert comparison["unit_label"] == "ml" + assert comparison["momo_total_quantity"] == 80 + assert comparison["competitor_total_quantity"] == 40 + assert comparison["momo_unit_price"] == 14.9875 + assert comparison["competitor_unit_price"] == 16.975 + assert comparison["unit_gap_pct"] < 0 + + def test_marketplace_matcher_does_not_unit_compare_multi_component_set(): from services.marketplace_product_matcher import score_marketplace_match @@ -123,6 +142,14 @@ def test_marketplace_matcher_does_not_unit_compare_multi_component_set(): assert diagnostics.comparison_mode == "not_comparable" assert "unit_comparable" not in diagnostics.reasons + comparison = score_marketplace_match( + "【蘭蔻】官方直營 玫瑰霜60ml+玫瑰精露150ml", + "【蘭蔻】絕對完美玫瑰霜 60ml", + momo_price=18765, + competitor_price=5349, + ) + assert comparison.comparison_mode == "not_comparable" + def test_marketplace_matcher_does_not_promote_wide_price_refill_candidate(): from services.marketplace_product_matcher import score_marketplace_match