fix: pick best targeted momo offer per pchome item
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.636"
|
||||
SYSTEM_VERSION = "V10.637"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -712,6 +712,31 @@ def _targeted_candidate_needs_review(candidate: dict[str, Any]) -> bool:
|
||||
return bool(reasons & review_reason_markers)
|
||||
|
||||
|
||||
def _targeted_candidate_sync_rank(candidate: dict[str, Any]) -> tuple[float, float, float, float]:
|
||||
"""同一個 PChome 商品有多個候選時,挑最適合營運判斷的一筆。"""
|
||||
auto_type = _targeted_candidate_auto_type(candidate)
|
||||
type_rank = 3.0 if auto_type == "total_price" else 2.0 if auto_type == "unit_price" else 0.0
|
||||
try:
|
||||
match_score = float(candidate.get("target_match_score") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
match_score = 0.0
|
||||
|
||||
unit_price_comparison = (
|
||||
candidate.get("target_unit_price_comparison")
|
||||
if isinstance(candidate.get("target_unit_price_comparison"), dict)
|
||||
else {}
|
||||
)
|
||||
momo_total = _to_float(unit_price_comparison.get("momo_total_quantity"))
|
||||
pchome_total = _to_float(unit_price_comparison.get("competitor_total_quantity"))
|
||||
quantity_delta = 999999.0
|
||||
same_quantity = 0.0
|
||||
if momo_total > 0 and pchome_total > 0:
|
||||
quantity_delta = abs(momo_total - pchome_total)
|
||||
same_quantity = 1.0 if quantity_delta <= 0.0001 else 0.0
|
||||
|
||||
return (type_rank, same_quantity, -quantity_delta, match_score)
|
||||
|
||||
|
||||
def _targeted_candidate_to_external_offer(
|
||||
candidate: dict[str, Any],
|
||||
*,
|
||||
@@ -839,7 +864,7 @@ def sync_targeted_momo_candidates_to_external_offers(
|
||||
|
||||
_ensure_external_market_source_seeds(conn)
|
||||
base_observed_at = datetime.now()
|
||||
offers: list[dict[str, Any]] = []
|
||||
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_candidate_to_external_offer(
|
||||
@@ -847,9 +872,16 @@ def sync_targeted_momo_candidates_to_external_offers(
|
||||
observed_at=base_observed_at + timedelta(microseconds=index),
|
||||
)
|
||||
if offer:
|
||||
offers.append(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:
|
||||
|
||||
@@ -361,6 +361,74 @@ def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeyp
|
||||
assert count == 0
|
||||
|
||||
|
||||
def test_sync_targeted_momo_candidates_keeps_best_unit_quantity_match(monkeypatch):
|
||||
from services import external_market_offer_service as service
|
||||
|
||||
monkeypatch.setattr(service, "mark_pchome_growth_cache_stale", lambda: None)
|
||||
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
_seed_external_offer_sync_tables(engine)
|
||||
|
||||
payload = service.sync_targeted_momo_candidates_to_external_offers(engine, [
|
||||
{
|
||||
"product_id": "MOMO-SINGLE",
|
||||
"name": "雪之上 全效合一水凝霜 80g/瓶",
|
||||
"price": 1380,
|
||||
"target_pchome_product_id": "PCH-YUKINOUE",
|
||||
"target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組",
|
||||
"target_pchome_price": 2960,
|
||||
"target_match_score": 0.74,
|
||||
"auto_compare_type": "unit_price",
|
||||
"target_price_basis": "unit_price",
|
||||
"target_match_reasons": ["count_conflict", "unit_comparable"],
|
||||
"target_comparison_mode": "unit_comparable",
|
||||
"target_unit_price_comparison": {
|
||||
"comparable": True,
|
||||
"unit_label": "g",
|
||||
"momo_total_quantity": 80,
|
||||
"competitor_total_quantity": 240,
|
||||
"momo_unit_price": 17.25,
|
||||
"competitor_unit_price": 12.33,
|
||||
"unit_gap_pct": 39.86,
|
||||
},
|
||||
},
|
||||
{
|
||||
"product_id": "MOMO-THREE",
|
||||
"name": "雪之上 全效合一水凝霜 80g X 3入瓶裝",
|
||||
"price": 2680,
|
||||
"target_pchome_product_id": "PCH-YUKINOUE",
|
||||
"target_pchome_name": "雪之上 全效合一水凝霜 80g 3入組",
|
||||
"target_pchome_price": 2960,
|
||||
"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": "g",
|
||||
"momo_total_quantity": 240,
|
||||
"competitor_total_quantity": 240,
|
||||
"momo_unit_price": 11.17,
|
||||
"competitor_unit_price": 12.33,
|
||||
"unit_gap_pct": -9.46,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
assert payload["success"] is True
|
||||
assert payload["candidate_count"] == 2
|
||||
assert payload["written_count"] == 1
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(text("""
|
||||
SELECT source_product_id, raw_payload_json
|
||||
FROM external_offers
|
||||
""")).mappings().one()
|
||||
raw_payload = __import__("json").loads(row["raw_payload_json"])
|
||||
assert row["source_product_id"] == "MOMO-THREE"
|
||||
assert raw_payload["unit_price_comparison"]["momo_total_quantity"] == 240
|
||||
|
||||
|
||||
def test_external_source_readiness_uses_legacy_momo_reference_cache():
|
||||
from services.external_market_offer_service import build_external_source_readiness
|
||||
|
||||
|
||||
Reference in New Issue
Block a user