V10.621 sync auto candidates to growth layer
Some checks failed
CD Pipeline / deploy (push) Failing after 34s

This commit is contained in:
OoO
2026-06-16 11:41:34 +08:00
parent 15010ab724
commit 01c73e02a2
14 changed files with 504 additions and 15 deletions

View File

@@ -247,6 +247,76 @@ def test_sync_legacy_momo_reference_offers_dry_run_does_not_write():
assert count == 0
def test_sync_targeted_momo_candidates_writes_unit_price_offer():
from services.external_market_offer_service import sync_targeted_momo_candidates_to_external_offers
engine = create_engine("sqlite:///:memory:")
_seed_external_offer_sync_tables(engine)
payload = sync_targeted_momo_candidates_to_external_offers(engine, [
{
"product_id": "10833188",
"name": "MOMO B5 修復霜 40ml",
"price": 468,
"original_price": 835,
"product_url": "https://momo.test/10833188",
"image_url": "https://img.test/10833188.jpg",
"brand": "理膚寶水",
"target_pchome_product_id": "PCH-1",
"target_pchome_name": "PChome B5 修復霜 40ml",
"target_pchome_price": 920,
"target_match_score": 0.74,
"auto_compare_type": "unit_price",
"target_price_basis": "unit_price",
"target_match_reasons": ["unit_comparable"],
"target_comparison_mode": "unit_comparable",
"target_unit_price_comparison": {
"comparable": True,
"unit_label": "ml",
"momo_total_quantity": 40,
"competitor_total_quantity": 40,
"momo_unit_price": 11.7,
"competitor_unit_price": 23.0,
"unit_gap_pct": -49.13,
},
},
{
"product_id": "REVIEW-1",
"name": "仍需人工商品",
"price": 500,
"target_pchome_product_id": "PCH-2",
"auto_compare_type": "manual_review",
},
])
assert payload["success"] is True
assert payload["status"] == "synced"
assert payload["candidate_count"] == 2
assert payload["written_count"] == 1
assert payload["unit_price_count"] == 1
assert payload["skipped_reasons"] == {"不是可自動使用的候選": 1}
with engine.connect() as conn:
row = conn.execute(text("""
SELECT source_product_id, price, pchome_product_id, match_status,
quality_score, data_quality_status, ingestion_method,
raw_payload_json
FROM external_offers
""")).mappings().one()
raw_payload = __import__("json").loads(row["raw_payload_json"])
assert row["source_product_id"] == "10833188"
assert row["price"] == 468
assert row["pchome_product_id"] == "PCH-1"
assert row["match_status"] == "verified"
assert row["quality_score"] == 82
assert row["data_quality_status"] == "verified"
assert row["ingestion_method"] == "targeted_momo_search"
assert raw_payload["price_basis"] == "unit_price"
assert raw_payload["pchome_public_price"] == 920
assert raw_payload["unit_price_comparison"]["unit_gap_pct"] == -49.13
def test_external_source_readiness_uses_legacy_momo_reference_cache():
from services.external_market_offer_service import build_external_source_readiness

View File

@@ -479,6 +479,8 @@ def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis():
assert "compSourceSummary" in template
assert "'external_offers':" in template
assert "'自動同步'" in template
assert "price_basis_label" in template
assert "商品總價" in template
assert "/api/ai/pchome-growth/opportunities" in template
assert "最近處理紀錄" in template
assert "處理紀錄" in template
@@ -529,6 +531,8 @@ def test_price_comparison_page_is_action_oriented_and_plain_chinese():
assert "renderMomoUnitComparePanel" in template
assert "自動單位價比較" in template
assert "查看單位價" in template
assert "sync_external_offers: true" in template
assert "已同步 ${syncedCount} 筆到作戰清單" in template
assert "renderMomoReviewPanel" in template
assert "/api/price_comparison/fetch_momo_for_pchome" in template
assert "MOMO 候選待確認" in template

View File

@@ -79,6 +79,7 @@ def test_search_momo_products_for_pchome_products_uses_each_pchome_product_as_ta
assert products[0]["product_id"] == "12345678"
assert products[0]["target_pchome_product_id"] == "PCH-1"
assert products[0]["target_pchome_name"] == "【LA ROCHE-POSAY 理膚寶水】B5全面修復霜 40ml"
assert products[0]["target_pchome_price"] == 920
assert products[0]["target_match_score"] > 0
assert products[0]["source_strategy"] == "pchome_targeted_momo_search"

View File

@@ -87,6 +87,46 @@ def _seed_growth_external_offers(engine):
"""))
def _seed_growth_unit_price_external_offer(engine):
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE external_offers (
id INTEGER PRIMARY KEY,
source_code TEXT,
platform_code TEXT,
source_product_id TEXT,
source_offer_key TEXT,
title TEXT,
price REAL,
observed_at TEXT,
expires_at TEXT,
ingestion_method TEXT,
pchome_product_id TEXT,
momo_sku TEXT,
match_status TEXT,
quality_score REAL,
data_quality_status TEXT,
quality_notes_json TEXT,
raw_payload_json TEXT
)
"""))
conn.execute(text("""
INSERT INTO external_offers (
id, source_code, platform_code, source_product_id, source_offer_key,
title, price, observed_at, expires_at, ingestion_method,
pchome_product_id, momo_sku, match_status, quality_score,
data_quality_status, quality_notes_json, raw_payload_json
)
VALUES (
1, 'momo_reference', 'momo', 'MOMO-UNIT', 'momo_reference:MOMO-UNIT:PCH-1:unit_price',
'MOMO 單位價商品', 468, '2026-06-14 12:00:00', NULL, 'targeted_momo_search',
'PCH-1', 'MOMO-UNIT', 'verified', 82,
'verified', '["自動單位價比較"]',
'{"price_basis": "unit_price", "pchome_public_price": 920, "pchome_public_name": "PChome 公開商品", "tags": ["identity_v2", "price_basis_unit_price"], "unit_price_comparison": {"unit_label": "ml", "momo_unit_price": 11.7, "competitor_unit_price": 23.0, "momo_total_quantity": 40, "competitor_total_quantity": 40, "unit_gap_pct": -49.13}}'
)
"""))
def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
@@ -138,6 +178,31 @@ def test_pchome_growth_prefers_external_offers_over_legacy_competitor_cache():
assert payload["stats"]["external_data_source_counts"] == {"自動同步資料層": 1}
def test_pchome_growth_understands_unit_price_external_offers():
from services.pchome_revenue_growth_service import build_pchome_growth_opportunities
engine = create_engine("sqlite:///:memory:")
_seed_growth_tables(engine)
_seed_growth_unit_price_external_offer(engine)
payload = build_pchome_growth_opportunities(engine, limit=5)
assert payload["success"] is True
pchome_1 = next(item for item in payload["opportunities"] if item["pchome_product_id"] == "PCH-1")
external_price = pchome_1["external_price"]
assert external_price["data_source"] == "external_offers"
assert external_price["price_basis"] == "unit_price"
assert external_price["price_basis_label"] == "單位價"
assert external_price["momo_price"] == 468
assert external_price["pchome_price"] == 920
assert external_price["momo_unit_price"] == 11.7
assert external_price["pchome_unit_price"] == 23.0
assert external_price["gap_pct"] == -49.1
assert pchome_1["recommended_action"]["label"] == "檢查售價與活動"
assert pchome_1["data_quality"]["label"] == "資料可用單位價判斷"
assert any("單位價" in line for line in pchome_1["reason_lines"])
def test_ai_product_pick_sales_join_by_sku_disabled_by_default(monkeypatch):
from services.ai_product_pick_agent import _sales_join_by_momo_sku_enabled

View File

@@ -172,3 +172,66 @@ def test_fetch_momo_for_pchome_endpoint_splits_auto_and_review_candidates(monkey
assert payload["data"]["products"][0]["product_id"] == "AUTO-1"
assert payload["data"]["unit_compare_candidates"][0]["product_id"] == "UNIT-1"
assert payload["data"]["review_candidates"][0]["product_id"] == "REVIEW-1"
assert payload["data"]["external_offer_sync"]["status"] == "not_requested"
def test_fetch_momo_for_pchome_endpoint_syncs_auto_candidates_when_requested(monkeypatch):
from routes import price_comparison_routes as routes
captured = {}
def fake_targeted_search(pchome_products, **kwargs):
return True, "找到候選", [
{
"name": "可直接比價商品",
"price": 890,
"product_id": "AUTO-1",
"auto_compare_type": "total_price",
"can_auto_compare": True,
},
{
"name": "自動單位價商品",
"price": 468,
"product_id": "UNIT-1",
"auto_compare_type": "unit_price",
"can_auto_compare": True,
},
{
"name": "需確認商品",
"price": 468,
"product_id": "REVIEW-1",
"can_auto_compare": False,
},
]
def fake_sync(candidates):
captured["sync_candidates"] = candidates
return {
"success": True,
"status": "synced",
"written_count": len(candidates),
"unit_price_count": 1,
"total_price_count": 1,
}
monkeypatch.setattr(routes, "search_momo_products_for_pchome_products", fake_targeted_search)
monkeypatch.setattr(routes, "_sync_targeted_candidates_to_external_offers", fake_sync)
app = Flask(__name__)
with app.test_request_context(
"/api/price_comparison/fetch_momo_for_pchome",
method="POST",
json={
"sync_external_offers": True,
"pchome_products": [
{"name": "理膚寶水 B5 修復霜", "price": 920, "product_id": "PCH-1"},
],
},
):
response = routes.fetch_momo_for_pchome_products.__wrapped__()
payload = response.get_json()
assert payload["success"] is True
assert payload["data"]["external_offer_sync"]["status"] == "synced"
assert payload["data"]["external_offer_sync"]["written_count"] == 2
assert [item["product_id"] for item in captured["sync_candidates"]] == ["AUTO-1", "UNIT-1"]