feat: persist targeted momo review candidates
Some checks failed
CD Pipeline / deploy (push) Failing after 35s
Some checks failed
CD Pipeline / deploy (push) Failing after 35s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.637"
|
||||
SYSTEM_VERSION = "V10.638"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
- V10.621 起 `/price_comparison` 的「自動找 MOMO 候選」會把可直接總價比價與自動單位價候選同步到 `external_offers`,`ingestion_method='targeted_momo_search'`,人工確認候選不得寫入。`external_offers.raw_payload_json.price_basis='unit_price'` 時,作戰清單必須使用 `unit_price_comparison` 的 MOMO / PChome 單位價與 `unit_gap_pct` 判斷價格壓力;不得把 MOMO 組合總價與 PChome 單品總價直接相減。此同步只影響外部價格參考與作戰清單,不寫 `competitor_prices`,也不自動改價。
|
||||
- V10.622 起任何 `external_offers` 自動同步成功寫入後,必須呼叫 `mark_pchome_growth_cache_stale()` 寫入共享 cache epoch;`/api/ai/pchome-growth/opportunities` 讀快取前必須比對 `get_pchome_growth_cache_epoch()`。這是跨 Gunicorn worker 的可見性保護,避免自動候選已進外部價格參考,但 AI 情報頁仍回 120 秒舊作戰清單。
|
||||
- V10.623 起 `/price_comparison` 與 `/ai_intelligence` 不得只靠大段文字說明流程:比價頁第一屏必須有主 KPI、目前卡點、四步流程與結果決策摘要;作戰頁第一屏必須有今日任務、可立即處理、待補比價與最新業績日。所有狀態都要由實際 API/前端狀態驅動,讓使用者一眼知道下一步要按哪個動作。
|
||||
- V10.638 起 PChome 導向 MOMO 補抓會把「找到但不能自動比價」的候選以 `match_status='needs_review'`、`data_quality_status='needs_review'` 保存到 `external_offers`;這些候選不得進價格壓力判斷,也不得發告警,但 `/api/ai/pchome-growth/opportunities` 可回傳待確認候選數,讓 UI 顯示「已有候選待確認」而不是只顯示無法比價。
|
||||
|
||||
## 零之一、12 Agent 決策信封(2026-05-24)
|
||||
|
||||
|
||||
@@ -834,6 +834,92 @@ def _targeted_candidate_to_external_offer(
|
||||
}, ""
|
||||
|
||||
|
||||
def _targeted_review_candidate_to_external_offer(
|
||||
candidate: dict[str, Any],
|
||||
*,
|
||||
observed_at: datetime,
|
||||
) -> tuple[dict[str, Any] | None, str]:
|
||||
"""保存待人工確認候選;這類資料不得進價格判斷。"""
|
||||
alert_tier = str(candidate.get("target_alert_tier") or "").strip()
|
||||
match_type = str(candidate.get("target_match_type") or "").strip()
|
||||
if alert_tier not in {"identity_review", "unit_price_review"} and match_type in {"", "no_match"}:
|
||||
return None, "不是可人工確認候選"
|
||||
|
||||
momo_sku = str(candidate.get("product_id") or candidate.get("goodsCode") or candidate.get("id") or "").strip()
|
||||
pchome_product_id = str(candidate.get("target_pchome_product_id") or "").strip()
|
||||
momo_price = _to_float(candidate.get("price"))
|
||||
if not momo_sku:
|
||||
return None, "缺少 MOMO 商品 ID"
|
||||
if not pchome_product_id:
|
||||
return None, "缺少 PChome 商品 ID"
|
||||
if not momo_price or momo_price <= 0:
|
||||
return None, "缺少 MOMO 售價"
|
||||
|
||||
match_score = _quality_score_from_match(candidate.get("target_match_score"))
|
||||
if match_score < 60:
|
||||
return None, "人工確認候選分數過低"
|
||||
|
||||
unit_price_comparison = (
|
||||
candidate.get("target_unit_price_comparison")
|
||||
if isinstance(candidate.get("target_unit_price_comparison"), dict)
|
||||
else {}
|
||||
)
|
||||
title = str(candidate.get("name") or candidate.get("title") or momo_sku).strip()
|
||||
price_basis = str(candidate.get("target_price_basis") or "manual_review").strip()
|
||||
raw_payload = {
|
||||
"source": "pchome_targeted_momo_search",
|
||||
"review_state": "needs_review",
|
||||
"auto_compare_type": "manual_review",
|
||||
"price_basis": price_basis,
|
||||
"pchome_public_price": _to_float(candidate.get("target_pchome_price")),
|
||||
"pchome_public_name": candidate.get("target_pchome_name"),
|
||||
"match_score": candidate.get("target_match_score"),
|
||||
"match_reasons": candidate.get("target_match_reasons") or [],
|
||||
"comparison_mode": candidate.get("target_comparison_mode"),
|
||||
"match_type": match_type,
|
||||
"alert_tier": alert_tier,
|
||||
"hard_veto": bool(candidate.get("target_hard_veto")),
|
||||
"target_gap_pct": candidate.get("target_gap_pct"),
|
||||
"unit_price_comparison": unit_price_comparison,
|
||||
"search_term": candidate.get("target_search_term"),
|
||||
"tags": [
|
||||
"identity_v2",
|
||||
"source_targeted_momo_search",
|
||||
"needs_review",
|
||||
f"review_{alert_tier or 'identity_review'}",
|
||||
],
|
||||
}
|
||||
return {
|
||||
"source_code": "momo_reference",
|
||||
"platform_code": "momo",
|
||||
"source_product_id": momo_sku,
|
||||
"source_offer_key": f"momo_reference:{momo_sku}:{pchome_product_id}:needs_review",
|
||||
"title": title or momo_sku,
|
||||
"brand": candidate.get("brand"),
|
||||
"category_text": candidate.get("category") or candidate.get("category_text"),
|
||||
"product_url": candidate.get("product_url") or candidate.get("url"),
|
||||
"image_url": candidate.get("image_url"),
|
||||
"price": momo_price,
|
||||
"original_price": _to_float(candidate.get("original_price")),
|
||||
"currency": "TWD",
|
||||
"stock_status": None,
|
||||
"sold_count": None,
|
||||
"rating": None,
|
||||
"review_count": None,
|
||||
"observed_at": observed_at,
|
||||
"expires_at": None,
|
||||
"ingestion_method": "targeted_momo_review",
|
||||
"connector_key": "pchome_targeted_momo_search_review",
|
||||
"pchome_product_id": pchome_product_id,
|
||||
"momo_sku": momo_sku,
|
||||
"match_status": "needs_review",
|
||||
"quality_score": round(match_score, 2),
|
||||
"data_quality_status": "needs_review",
|
||||
"quality_notes_json": json.dumps(["候選已找到,需人工確認同款或色號"], ensure_ascii=False),
|
||||
"raw_payload_json": json.dumps(raw_payload, ensure_ascii=False),
|
||||
}, ""
|
||||
|
||||
|
||||
def sync_targeted_momo_candidates_to_external_offers(
|
||||
engine,
|
||||
candidates: list[dict[str, Any]],
|
||||
@@ -914,6 +1000,76 @@ def sync_targeted_momo_candidates_to_external_offers(
|
||||
}
|
||||
|
||||
|
||||
def sync_targeted_momo_review_candidates_to_external_offers(
|
||||
engine,
|
||||
candidates: list[dict[str, Any]],
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""把待確認 MOMO 候選保存起來,供人工審核;不進價格告警。"""
|
||||
generated_at = datetime.now().isoformat(timespec="seconds")
|
||||
candidates = list(candidates or [])
|
||||
required_tables = {"external_market_sources", "external_offers"}
|
||||
|
||||
with engine.begin() as conn:
|
||||
missing_tables = sorted(table for table in required_tables if not _has_table(conn, table))
|
||||
if missing_tables:
|
||||
return {
|
||||
"success": False,
|
||||
"status": "skipped",
|
||||
"generated_at": generated_at,
|
||||
"candidate_count": len(candidates),
|
||||
"written_count": 0,
|
||||
"dry_run": dry_run,
|
||||
"message": "待確認候選暫時無法保存,缺少必要資料表。",
|
||||
"missing_tables": missing_tables,
|
||||
}
|
||||
|
||||
_ensure_external_market_source_seeds(conn)
|
||||
base_observed_at = datetime.now()
|
||||
ranked_offers: list[tuple[dict[str, Any], tuple[float, float, float, float]]] = []
|
||||
skipped_reasons: dict[str, int] = {}
|
||||
for index, candidate in enumerate(candidates):
|
||||
offer, reason = _targeted_review_candidate_to_external_offer(
|
||||
candidate,
|
||||
observed_at=base_observed_at + timedelta(microseconds=index),
|
||||
)
|
||||
if offer:
|
||||
ranked_offers.append((offer, _targeted_candidate_sync_rank(candidate)))
|
||||
else:
|
||||
skipped_reasons[reason] = skipped_reasons.get(reason, 0) + 1
|
||||
|
||||
selected_by_pchome: dict[str, tuple[dict[str, Any], tuple[float, float, float, float]]] = {}
|
||||
for offer, rank in ranked_offers:
|
||||
key = str(offer.get("pchome_product_id") or offer.get("source_offer_key") or "").strip()
|
||||
existing = selected_by_pchome.get(key)
|
||||
if existing is None or rank > existing[1]:
|
||||
selected_by_pchome[key] = (offer, rank)
|
||||
offers = [offer for offer, _ in selected_by_pchome.values()]
|
||||
|
||||
if not dry_run:
|
||||
for offer in offers:
|
||||
_upsert_external_offer(conn, offer)
|
||||
if offers:
|
||||
mark_pchome_growth_cache_stale()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "dry_run" if dry_run else "synced",
|
||||
"generated_at": generated_at,
|
||||
"candidate_count": len(candidates),
|
||||
"written_count": 0 if dry_run else len(offers),
|
||||
"dry_run": dry_run,
|
||||
"source_code": "momo_reference",
|
||||
"skipped_reasons": skipped_reasons,
|
||||
"message": (
|
||||
"已保存待人工確認的 MOMO 候選。"
|
||||
if not dry_run
|
||||
else "已完成待確認候選預檢,尚未寫入資料。"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def sync_legacy_momo_reference_offers(engine, *, limit: int = 500, dry_run: bool = False) -> dict[str, Any]:
|
||||
"""把既有已確認同款的比價快取自動同步到 external_offers。"""
|
||||
limit = max(1, min(int(limit or 500), 5000))
|
||||
@@ -1172,6 +1328,11 @@ def _normalized_offer_stats(conn) -> dict[str, dict[str, Any]]:
|
||||
AND COALESCE(o.quality_score, 0) >= 76
|
||||
THEN 1 ELSE 0 END
|
||||
) AS usable_offer_count,
|
||||
SUM(CASE
|
||||
WHEN o.match_status = 'needs_review'
|
||||
OR o.data_quality_status = 'needs_review'
|
||||
THEN 1 ELSE 0 END
|
||||
) AS review_offer_count,
|
||||
MAX(o.observed_at) AS last_seen_at
|
||||
FROM external_market_sources s
|
||||
LEFT JOIN external_offers o ON o.source_code = s.code
|
||||
@@ -1184,6 +1345,7 @@ def _normalized_offer_stats(conn) -> dict[str, dict[str, Any]]:
|
||||
"enabled": bool(row.get("enabled")),
|
||||
"offer_count": int(row.get("offer_count") or 0),
|
||||
"usable_offer_count": int(row.get("usable_offer_count") or 0),
|
||||
"review_offer_count": int(row.get("review_offer_count") or 0),
|
||||
"last_seen_at": str(row.get("last_seen_at") or "") or None,
|
||||
}
|
||||
for row in rows
|
||||
@@ -1244,16 +1406,23 @@ def build_external_source_readiness(engine=None) -> dict[str, Any]:
|
||||
|
||||
source["schema_ready"] = schema_ready
|
||||
source["usable_offer_count"] = usable
|
||||
source["review_offer_count"] = int(stats.get("review_offer_count") or 0)
|
||||
source["last_seen_at"] = last_seen_at
|
||||
source["can_alert"] = source["status_code"] == "active" and usable > 0
|
||||
if source["status_code"] == "active":
|
||||
source["plain_state"] = "已接入,可進作戰清單" if usable else "已接入,等待可用資料"
|
||||
if usable:
|
||||
source["plain_state"] = "已接入,可進作戰清單"
|
||||
elif source["review_offer_count"]:
|
||||
source["plain_state"] = "已有待確認候選"
|
||||
else:
|
||||
source["plain_state"] = "已接入,等待可用資料"
|
||||
else:
|
||||
source["plain_state"] = "先保留接口,不進告警"
|
||||
|
||||
active_count = sum(1 for source in sources if source["status_code"] == "active")
|
||||
paused_count = sum(1 for source in sources if source["status_code"] == "paused")
|
||||
usable_count = sum(int(source["usable_offer_count"]) for source in sources)
|
||||
review_offer_count = sum(int(source.get("review_offer_count") or 0) for source in sources)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -1261,6 +1430,7 @@ def build_external_source_readiness(engine=None) -> dict[str, Any]:
|
||||
"active_count": active_count,
|
||||
"paused_count": paused_count,
|
||||
"usable_offer_count": usable_count,
|
||||
"review_offer_count": review_offer_count,
|
||||
"sources": sources,
|
||||
"connector_contract": build_connector_contracts(),
|
||||
"plain_summary": "MOMO 先用;蝦皮與酷澎先保留接口,暫不進告警。",
|
||||
|
||||
@@ -97,6 +97,12 @@ def _default_sync_candidates(engine, candidates: list[dict[str, Any]]) -> dict[s
|
||||
return sync_targeted_momo_candidates_to_external_offers(engine, candidates, dry_run=False)
|
||||
|
||||
|
||||
def _default_sync_review_candidates(engine, candidates: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
from services.external_market_offer_service import sync_targeted_momo_review_candidates_to_external_offers
|
||||
|
||||
return sync_targeted_momo_review_candidates_to_external_offers(engine, candidates, dry_run=False)
|
||||
|
||||
|
||||
def run_pchome_growth_momo_backfill(
|
||||
engine,
|
||||
*,
|
||||
@@ -104,16 +110,18 @@ def run_pchome_growth_momo_backfill(
|
||||
build_payload_func: Callable[[Any, int], dict[str, Any]] | None = None,
|
||||
search_func: Callable[[list[dict[str, Any]], int], tuple[bool, str, list[dict[str, Any]]]] | None = None,
|
||||
sync_func: Callable[[Any, list[dict[str, Any]]], dict[str, Any]] | None = None,
|
||||
sync_review_func: Callable[[Any, list[dict[str, Any]]], dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""補高業績 PChome 商品的 MOMO 對應。
|
||||
|
||||
不呼叫 LLM,只搜尋 MOMO 候選,並只把可自動判斷的 total_price / unit_price
|
||||
寫入 external_offers;需人工確認的候選只回報、不寫入。
|
||||
寫入 external_offers;需人工確認的候選會以 needs_review 保存,不進價格判斷。
|
||||
"""
|
||||
limit = max(1, min(int(limit or 12), 20))
|
||||
build_payload = build_payload_func or _default_build_payload
|
||||
search_candidates = search_func or _default_search_candidates
|
||||
sync_candidates = sync_func or _default_sync_candidates
|
||||
sync_review_candidates = sync_review_func or _default_sync_review_candidates
|
||||
|
||||
before_payload = build_payload(engine, max(limit, 16))
|
||||
targets = build_momo_backfill_targets(before_payload, limit)
|
||||
@@ -135,6 +143,12 @@ def run_pchome_growth_momo_backfill(
|
||||
"written_count": 0,
|
||||
"message": "沒有需要同步的自動候選。",
|
||||
},
|
||||
"review_candidate_sync": {
|
||||
"success": True,
|
||||
"status": "not_needed",
|
||||
"written_count": 0,
|
||||
"message": "沒有需要保存的待確認候選。",
|
||||
},
|
||||
"before_stats": before_payload.get("stats") or {},
|
||||
"after_stats": before_payload.get("stats") or {},
|
||||
"targets": [],
|
||||
@@ -165,6 +179,14 @@ def run_pchome_growth_momo_backfill(
|
||||
}
|
||||
if auto_candidates:
|
||||
external_offer_sync = sync_candidates(engine, auto_candidates)
|
||||
review_candidate_sync = {
|
||||
"success": True,
|
||||
"status": "not_found",
|
||||
"written_count": 0,
|
||||
"message": "沒有需要保存的待確認候選。",
|
||||
}
|
||||
if review_candidates:
|
||||
review_candidate_sync = sync_review_candidates(engine, review_candidates)
|
||||
|
||||
after_payload = build_payload(engine, max(limit, 16))
|
||||
written_count = int(external_offer_sync.get("written_count") or 0)
|
||||
@@ -189,6 +211,7 @@ def run_pchome_growth_momo_backfill(
|
||||
"auto_compare_count": len(auto_candidates),
|
||||
"review_count": len(review_candidates),
|
||||
"external_offer_sync": external_offer_sync,
|
||||
"review_candidate_sync": review_candidate_sync,
|
||||
"before_stats": before_payload.get("stats") or {},
|
||||
"after_stats": after_payload.get("stats") or {},
|
||||
"targets": targets[:8],
|
||||
|
||||
@@ -703,6 +703,7 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
||||
limit = max(5, min(int(limit or 20), 50))
|
||||
generated_at = datetime.now().isoformat(timespec="seconds")
|
||||
source_readiness = build_external_source_readiness(engine)
|
||||
review_candidate_count = int(source_readiness.get("review_offer_count") or 0)
|
||||
source_scope = {
|
||||
"primary_goal": "提升 PChome 業績",
|
||||
"primary_sales_source": PRIMARY_SALES_SOURCE,
|
||||
@@ -724,6 +725,7 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
||||
"mapped_count": 0,
|
||||
"mapping_rate": 0,
|
||||
"needs_mapping_count": 0,
|
||||
"review_candidate_count": review_candidate_count,
|
||||
},
|
||||
"opportunities": [],
|
||||
"message": "目前還沒有 PChome 業績資料,請先完成業績匯入。",
|
||||
@@ -745,6 +747,7 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
||||
"mapped_count": 0,
|
||||
"mapping_rate": 0,
|
||||
"needs_mapping_count": 0,
|
||||
"review_candidate_count": review_candidate_count,
|
||||
"total_sales_7d": 0,
|
||||
"opportunity_sales_7d": 0,
|
||||
"action_counts": {},
|
||||
@@ -794,6 +797,7 @@ def build_pchome_growth_opportunities(engine, limit: int = 20) -> dict[str, Any]
|
||||
"mapped_count": mapped_count,
|
||||
"mapping_rate": mapping_rate,
|
||||
"needs_mapping_count": needs_mapping_count,
|
||||
"review_candidate_count": review_candidate_count,
|
||||
"total_sales_7d": round(sum(_to_float(item.get("sales_7d")) for item in opportunities), 2),
|
||||
"opportunity_sales_7d": round(sum(_to_float(item.get("sales_7d")) for item in opportunities), 2),
|
||||
"action_counts": action_counts,
|
||||
|
||||
@@ -1196,6 +1196,10 @@
|
||||
<strong id="growthNeedsMapping">—</strong>
|
||||
<span>無法比價</span>
|
||||
</div>
|
||||
<div class="growth-metric">
|
||||
<strong id="growthReviewCandidateCount">—</strong>
|
||||
<span>待確認</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="growth-action-hint" id="growthActionHint">正在判斷今天優先處理順序...</p>
|
||||
<p class="growth-data-summary" id="growthDataSourceSummary">來源整理中...</p>
|
||||
@@ -1724,6 +1728,7 @@ async function loadGrowthOps(forceRefresh = false) {
|
||||
document.getElementById('growthCandidateCount').textContent = (stats.candidate_count || 0).toLocaleString();
|
||||
document.getElementById('growthMappedCount').textContent = (stats.mapped_count || 0).toLocaleString();
|
||||
document.getElementById('growthNeedsMapping').textContent = (stats.needs_mapping_count || 0).toLocaleString();
|
||||
document.getElementById('growthReviewCandidateCount').textContent = (stats.review_candidate_count || 0).toLocaleString();
|
||||
renderOpsCommandDashboard(stats, scope);
|
||||
renderGrowthActionHint(stats);
|
||||
renderGrowthDataSourceSummary(stats);
|
||||
@@ -1768,6 +1773,7 @@ function renderGrowthActionHint(stats) {
|
||||
const candidateCount = Number(stats.candidate_count || 0);
|
||||
const mappedCount = Number(stats.mapped_count || 0);
|
||||
const needsMapping = Number(stats.needs_mapping_count || 0);
|
||||
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
|
||||
|
||||
if (!candidateCount) {
|
||||
hint.textContent = '還不能產生今日清單,原因是缺少最新 PChome 業績。';
|
||||
@@ -1779,6 +1785,11 @@ function renderGrowthActionHint(stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsMapping > 0 && reviewCandidateCount > 0) {
|
||||
hint.textContent = `先處理 ${mappedCount.toLocaleString()} 件可立即處理商品;另有 ${reviewCandidateCount.toLocaleString()} 筆候選等人工確認。`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsMapping > 0) {
|
||||
hint.textContent = `先處理 ${mappedCount.toLocaleString()} 件可立即處理商品,再補 ${needsMapping.toLocaleString()} 件比價資料。`;
|
||||
return;
|
||||
|
||||
@@ -361,6 +361,74 @@ def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeyp
|
||||
assert count == 0
|
||||
|
||||
|
||||
def test_sync_targeted_momo_review_candidates_writes_needs_review_offer(monkeypatch):
|
||||
from services import external_market_offer_service as service
|
||||
|
||||
stale_marks = []
|
||||
monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: stale_marks.append(True))
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
_seed_external_offer_sync_tables(engine)
|
||||
|
||||
payload = service.sync_targeted_momo_review_candidates_to_external_offers(engine, [
|
||||
{
|
||||
"product_id": "14917079",
|
||||
"name": "【cle de peau 肌膚之鑰】光采柔焦蜜粉 24g (國際航空版)",
|
||||
"price": 2618,
|
||||
"target_pchome_product_id": "PCH-CDP",
|
||||
"target_pchome_name": "cle de peau 光采柔焦蜜粉 24g #1",
|
||||
"target_pchome_price": 2790,
|
||||
"target_match_score": 1.0,
|
||||
"auto_compare_type": "manual_review",
|
||||
"target_price_basis": "none",
|
||||
"target_alert_tier": "identity_review",
|
||||
"target_match_type": "exact",
|
||||
"target_match_reasons": ["variant_selection_review", "strong_exact_spec_match"],
|
||||
"target_comparison_mode": "exact_identity",
|
||||
"target_gap_pct": -6.16,
|
||||
},
|
||||
{
|
||||
"product_id": "LOW-SCORE",
|
||||
"name": "低分候選",
|
||||
"price": 100,
|
||||
"target_pchome_product_id": "PCH-LOW",
|
||||
"target_match_score": 0.4,
|
||||
"target_alert_tier": "identity_review",
|
||||
},
|
||||
])
|
||||
|
||||
assert payload["success"] is True
|
||||
assert payload["status"] == "synced"
|
||||
assert payload["candidate_count"] == 2
|
||||
assert payload["written_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()
|
||||
|
||||
readiness = service.build_external_source_readiness(engine)
|
||||
|
||||
raw_payload = __import__("json").loads(row["raw_payload_json"])
|
||||
assert row["source_product_id"] == "14917079"
|
||||
assert row["price"] == 2618
|
||||
assert row["pchome_product_id"] == "PCH-CDP"
|
||||
assert row["match_status"] == "needs_review"
|
||||
assert row["quality_score"] == 100
|
||||
assert row["data_quality_status"] == "needs_review"
|
||||
assert row["ingestion_method"] == "targeted_momo_review"
|
||||
assert raw_payload["review_state"] == "needs_review"
|
||||
assert raw_payload["price_basis"] == "none"
|
||||
assert raw_payload["alert_tier"] == "identity_review"
|
||||
assert "needs_review" in raw_payload["tags"]
|
||||
assert readiness["review_offer_count"] == 1
|
||||
assert stale_marks == [True]
|
||||
|
||||
|
||||
def test_sync_targeted_momo_candidates_keeps_best_unit_quantity_match(monkeypatch):
|
||||
from services import external_market_offer_service as service
|
||||
|
||||
|
||||
@@ -307,12 +307,21 @@ def test_pchome_growth_momo_backfill_service_targets_unmapped_high_sales_items()
|
||||
"unit_price_count": 1,
|
||||
}
|
||||
|
||||
def fake_sync_review(engine, candidates):
|
||||
captured["review_sync_candidates"] = candidates
|
||||
return {
|
||||
"success": True,
|
||||
"status": "synced",
|
||||
"written_count": len(candidates),
|
||||
}
|
||||
|
||||
payload = run_pchome_growth_momo_backfill(
|
||||
FakeEngine(),
|
||||
limit=2,
|
||||
build_payload_func=fake_build_payload,
|
||||
search_func=fake_search,
|
||||
sync_func=fake_sync,
|
||||
sync_review_func=fake_sync_review,
|
||||
)
|
||||
|
||||
assert payload["success"] is True
|
||||
@@ -321,10 +330,12 @@ def test_pchome_growth_momo_backfill_service_targets_unmapped_high_sales_items()
|
||||
assert payload["data"]["auto_compare_count"] == 2
|
||||
assert payload["data"]["review_count"] == 1
|
||||
assert payload["data"]["external_offer_sync"]["written_count"] == 2
|
||||
assert payload["data"]["review_candidate_sync"]["written_count"] == 1
|
||||
assert payload["data"]["after_stats"]["mapping_rate"] == 66.7
|
||||
assert [item["product_id"] for item in captured["targets"]] == ["PCH-NEEDS-1", "PCH-NEEDS-2"]
|
||||
assert [item["price"] for item in captured["targets"]] == [920, 760]
|
||||
assert [item["product_id"] for item in captured["sync_candidates"]] == ["MOMO-AUTO", "MOMO-UNIT"]
|
||||
assert [item["product_id"] for item in captured["review_sync_candidates"]] == ["MOMO-REVIEW"]
|
||||
assert captured["search_limit"] == 2
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user