from pathlib import Path import logging from datetime import datetime ROOT = Path(__file__).resolve().parents[1] def test_competitor_feeder_persists_all_match_attempt_outcomes(): source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8") migration = (ROOT / "migrations/023_competitor_match_attempts.sql").read_text(encoding="utf-8") assert "attempts_written" in source assert "_ensure_competitor_match_attempts_table" in source assert "_record_match_attempt" in source assert "INSERT INTO competitor_match_attempts" in source assert "CAST(:search_terms AS jsonb)" in source assert 'attempt_status="matched"' in source assert 'attempt_status="low_score"' in source 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, 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 "_fetch_expired_identity_skus" in source assert "run_expired_identity_refresh" in source 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 assert "CREATE TABLE IF NOT EXISTS competitor_match_attempts" in migration assert "attempt_status" in migration assert "search_terms" in migration assert "best_match_score" in migration assert "error_message" in migration assert "idx_comp_match_attempts_sku_source_time" in migration def test_competitor_feeder_logs_keyword_parser_fallback(monkeypatch, caplog): from services import competitor_price_feeder from services import marketplace_product_matcher def broken_build_search_terms(*_args, **_kwargs): raise RuntimeError("matcher unavailable") 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") assert terms assert "fallback to cleaned product name" in caplog.text def test_competitor_feeder_refreshes_expired_identity_by_known_product_id(monkeypatch): from services.competitor_price_feeder import CompetitorPriceFeeder from services.pchome_crawler import PChomeProduct requested = [] product = PChomeProduct( product_id="DDAB01-1900ABCD", name="舒特膚 AD 乳液 200ml", price=899, original_price=999, discount=10, image_url="", product_url="https://24h.pchome.com.tw/prod/DDAB01-1900ABCD", stock=50, store="24h", rating=4.8, review_count=12, 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): requested.extend(product_ids) return True, "ok", [product] monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler) feeder = CompetitorPriceFeeder(engine=object()) writes = [] attempts = [] 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": "A001", "name": "舒特膚 AD 乳液 200ml", "product_id": 1, "momo_price": 980, "competitor_product_id": "DDAB01-1900ABCD", }]) assert requested == ["DDAB01-1900ABCD"] assert result.matched == 1 assert writes[0]["product_id"] == "DDAB01-1900ABCD" assert "identity_v2" in writes[0]["tags"] assert "refresh_known_identity" in writes[0]["tags"] assert attempts[0]["attempt_status"] == "matched" assert attempts[0]["search_terms"] == ["known_product_id:DDAB01-1900ABCD"] def test_competitor_feeder_records_unit_comparable_without_price_upsert(monkeypatch): from services.competitor_price_feeder import CompetitorPriceFeeder from services.pchome_crawler import PChomeProduct product = PChomeProduct( product_id="DDAB01-UNIT", name="理膚寶水 全面修復霜 B5 40ml", price=679, original_price=799, discount=15, image_url="", product_url="https://24h.pchome.com.tw/prod/DDAB01-UNIT", stock=20, store="24h", rating=4.7, review_count=8, is_on_sale=True, crawled_at=datetime.now(), ) class FakeCrawler: def __init__(self, *_args, **_kwargs): pass def search_products(self, *_args, **_kwargs): return True, "ok", [product] monkeypatch.setattr("services.pchome_crawler.PChomeCrawler", FakeCrawler) feeder = CompetitorPriceFeeder(engine=object()) attempts = [] writes = [] monkeypatch.setattr( feeder, "_record_match_attempt", lambda *args, **kwargs: attempts.append(kwargs), ) monkeypatch.setattr( feeder, "_upsert_competitor_price", lambda *args, **kwargs: writes.append((args, kwargs)), ) result = feeder._run_sku_items([{ "sku": "A002", "name": "理膚寶水 B5 全面修復霜 40ml x2 超值組", "product_id": 2, "momo_price": 1199, }]) assert result.matched == 0 assert result.skipped_low_score == 1 assert writes == [] assert attempts[0]["attempt_status"] == "unit_comparable" assert "unit_comparable" in attempts[0]["error_message"]