From c091c466c203d7e26c8ed576f573e7e930df34d1 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 13:15:33 +0800 Subject: [PATCH] fix(growth): partition Yahoo promotion candidates --- config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 8 +- .../ai_automation_mainline_work_items.md | 14 +-- .../pchome_growth_momo_backfill_service.py | 29 +++++- .../yahoo_shopping_public_offer_service.py | 1 + tests/test_pchome_revenue_growth_service.py | 98 +++++++++++++++++++ 6 files changed, 139 insertions(+), 13 deletions(-) diff --git a/config.py b/config.py index 7684049..76a5a79 100644 --- a/config.py +++ b/config.py @@ -414,7 +414,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.800" +SYSTEM_VERSION = "V10.801" 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 44126f1..9a8dcf2 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -1,18 +1,18 @@ # PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth > **最後更新**: 2026-07-15 (台北時間) -> **狀態**: 🟠 Partial。V10.799 已在 production 以 exact Gitea object `01329c7048ffb54d7f323e84d8c9de086eb1cd68` 運行。`pchome_comparison_coverage_v3` 把 TOP50 固定為 action-priority scoring 前的營收排名母體,mapping 狀態不再把已完成商品擠出分母;production 最終 readback 為 `21/50 = 42%` 商品覆蓋與 `NT$180,667 / NT$354,062 = 51.0%` 業績覆蓋,母體 fingerprint `7b74504fcc1e1801c2ca2b42` 在三輪 bounded run 前後不變。V10.800 Yahoo Shopping structured adapter 已完成 source candidate,workstation regression 為 `2,105 passed / 9 skipped / 0 failed`,但尚未部署或改變 production KPI;正式外部平台 runtime 仍僅 `1/15`。SEC-P0-001 已完成 production closure,SEC-P0-002 維持 production hybrid canary-ready;full asset reconciliation、software supply chain、統一 controlled-apply envelope、restore drill、internal RAG canary 與 MCP/RAG runtime closure 尚未全部完成。任何局部 receipt 不得代表整體閉環完成。 -> **適用版本**: V10.799 production runtime + V10.800 source candidate;Gitea Action #1131 仍因 `ewoooc-host` runner 離線而未提供 CD 執行證據 +> **狀態**: 🟠 Partial。V10.800 已在 production 以 exact Gitea object `4670f80f98f705050784f26adf7797b54e4c409f` 運行。`pchome_comparison_coverage_v3` production readback 維持 `21/50 = 42%` 商品覆蓋與 `NT$180,667 / NT$354,062 = 51.0%` 業績覆蓋,母體 fingerprint `7b74504fcc1e1801c2ca2b42` 未漂移。Yahoo run `70857a03b2ae49bbbd6683d3781441f8` 安全 no-write;run `a950e4b209234088b88bc35b70b3bbda` 的 check-mode 找到 `3 exact + 1 unit-price` 並因 source promotion policy 混批而零寫入,正式外部平台 runtime 仍僅 `1/15`。V10.801 source candidate 已把 unit-price 自動分流到 candidate-only、exact 保留 controlled apply,完整 regression 為 `2,106 passed / 9 skipped / 0 failed`。SEC-P0-001 已完成 production closure,SEC-P0-002 維持 production hybrid canary-ready;full asset reconciliation、software supply chain、統一 controlled-apply envelope、restore drill、internal RAG canary 與 MCP/RAG runtime closure 尚未全部完成。任何局部 receipt 不得代表整體閉環完成。 +> **適用版本**: V10.800 production runtime + V10.801 source candidate;Gitea Action #1132 仍因 `ewoooc-host` runner 離線而未提供 CD 執行證據 --- -## 零之負四、Yahoo Shopping 公開結構化同品 canary(V10.800 source candidate) +## 零之負四、Yahoo Shopping 公開結構化同品 canary(V10.800 production / V10.801 source) - Yahoo 搜尋只解析公開頁面的 `script#isoredux-data`,商品頁只解析 schema.org `Product` / `Offer` JSON-LD;PChome 目標商品則由公開 JSONP product API 回讀 `Name`、`Spec`、`Price` 與 `Qty`。所有 URL 都受 HTTPS host/path allowlist、回應大小、timeout、查詢數、detail 數與 request interval 約束,不讀登入 cookie/session,也不繞過平台阻擋。 - 正式候選必須同時通過 search/detail product ID 一致、TWD 正價、PChome current price、雙方庫存(有證據時)、PChome Spec 出現在來源標題、非「任選/多款/可選」、非括號多規格組、deterministic exact same-item matcher 與獨立 DB post-verifier。只有 exact offer 能寫 `external_offers`;其餘候選只留 receipt,不因 KPI 壓力放寬 matcher。 - Controlled apply 強制先做零寫入 check-mode,再以同一 `trace_id/run_id/work_item_id` bounded execute;至少兩筆 exact offer 全數獨立回讀成功後,才一次啟用 `active + enabled + write_enabled`。來源尚未啟用但 post-verifier 失敗時也會 quarantine 同 run row;activation 後失敗則同時還原 source state,不刪除歷史資料。compact atomic receipt 由 scheduler 每六小時更新,API/CLI 與 UI 分開顯示正式來源、候選證據與任何 runtime evidence。 - 最新 workstation 公開 live no-write probe 對 OBgE、Amywo、Lancome、Sebamed 取得三筆 in-stock source-guard/total-price exact 候選,Lancome 因 Yahoo 明確 `ec_has_stock=0` 被阻擋。Critic 另以 Estee 色號、Armani 色號及 Gatsby 香型做反例;唯一曾漏過的括號多規格 listing 已加 guard,Gatsby 聚焦重跑的三筆候選全數阻擋。PChome sales-report fallback 不得冒充 current public price;公開 HTTP 採真正 bounded streaming。這些都是 source validation,不是 production DB、正式 coverage 或平台 activation 證據。 -- Production 仍為 V10.799、`21/50` 商品覆蓋、`51.0%` 業績覆蓋與 `1/15` 正式平台 runtime。V10.800 已通過 `2,105 passed / 9 skipped / 0 failed` 完整 regression;剩餘 exit 必須依序完成 Gitea main/dev、部署 readback、production check-mode、bounded canary、獨立 DB verifier、TOP50 fingerprint/KPI delta、三應用容器健康與 `momo-db` identity unchanged。 +- V10.800 已由 exact archive `d660339f30727f62bf2f39eae2665fe6b832f77e6743743bc8ea85b6206dbc05` 部署,16 個 runtime hash、內外 `/health`、三應用容器、scheduler 六小時註冊與 `momo-db` identity unchanged 均已驗證。Limit-8 run `70857a03b2ae49bbbd6683d3781441f8` 為 `no_write_verified`;limit-20 run `a950e4b209234088b88bc35b70b3bbda` 在 check-mode 找到 4 個獨立 verifier 候選,其中 3 個 exact、1 個 unit-price,因 Yahoo 只允許 total-price 正式 promotion 而整批 `check_mode_failed_no_write`。V10.801 將 source-disallowed compare type 自動降為 candidate-only,formal apply 只接 exact;完整 regression 為 `2,106 passed / 9 skipped / 0 failed`。Production KPI 仍為 `21/50`、`51.0%` 與 `1/15`,不得提前宣稱 activation。 --- diff --git a/docs/guides/ai_automation_mainline_work_items.md b/docs/guides/ai_automation_mainline_work_items.md index e38179d..e279192 100644 --- a/docs/guides/ai_automation_mainline_work_items.md +++ b/docs/guides/ai_automation_mainline_work_items.md @@ -1,6 +1,6 @@ # AI Automation Mainline Work Items -> Updated: 2026-07-15 12:39 Asia/Taipei +> Updated: 2026-07-15 13:12 Asia/Taipei > Governance: `global_product_governance_v2` + ADR-038 > Current P0: `GROWTH-P0-001 comparison coverage truth + autonomous refresh` @@ -17,7 +17,7 @@ | Order | ID | Status | Work item | Exit evidence / next machine action | |---:|---|---|---|---| | 1 | `SEC-P0-001` | Completed | Deny-by-default route access control | `governance/evidence/SEC-P0-001-20260711T122758Z.json` plus the current runtime receipt prove anonymous matrix 8/8 denied, public `/metrics` 404, exact internal target up, EwoooC product markers present, Prometheus identity preserved and `momo-db` unchanged. | -| 2 | `GROWTH-P0-001` | In progress (`runtime_partial`) | Comparison coverage truth + autonomous refresh | V10.799 is live. `pchome_comparison_coverage_v3` fixes TOP50 membership before action-priority scoring; fingerprint `7b74504fcc1e1801c2ca2b42` stayed unchanged across all bounded runs. Final production truth is `21 ready + 1 candidate validation + 28 unmatched`, count coverage `42%`, and revenue coverage `NT$180,667 / NT$354,062 = 51.0%`. V10.800 source candidate adds a bounded Yahoo Shopping public structured adapter, enforced zero-write check-mode, exact same-item guards, independent DB verifier, automatic activation/rollback, six-hour scheduler and formal-versus-candidate UI evidence. Final regression is `2,105 passed / 9 skipped / 0 failed`; latest four-target live no-write probe produced three exact in-stock candidates and correctly blocked one explicit out-of-stock Lancome offer. Next: merge through Gitea, deploy V10.800 and execute the bounded production canary before updating any runtime KPI. | +| 2 | `GROWTH-P0-001` | In progress (`runtime_partial`) | Comparison coverage truth + autonomous refresh | V10.800 is live at Gitea object `4670f80f98f705050784f26adf7797b54e4c409f`. Production run `70857a03b2ae49bbbd6683d3781441f8` scanned the first 8 unresolved items and closed `no_write_verified`; run `a950e4b209234088b88bc35b70b3bbda` scanned 20, found `3 exact + 1 unit-price` independently verified candidates, but check-mode correctly blocked the mixed promotion batch with zero formal writes. Production truth therefore remains `21 ready + 1 candidate validation + 28 unmatched`, `42%` count, `51.0%` revenue and `1/15` formal platforms. V10.801 source candidate routes source-disallowed unit-price matches to candidate-only receipts while exact matches continue to controlled apply; full regression is `2,106 passed / 9 skipped / 0 failed`. Next: deploy V10.801 and rerun the same bounded limit-20 canary before updating any KPI. | | 3 | `SEC-P0-002` | In progress (`canary_ready`) | Database identity + least-privilege RBAC | V10.789 is live and `governance/auth_identity_runtime_receipt.json` verifies required tables, two active admins, durable lockout, session revocation, trusted proxy policy and no-secret mutation audit readiness. Runtime is intentionally hybrid because zero database-admin success receipts have been captured; `auto` retires shared authority after two durable successes without a manual review gate. Next: capture database-admin login receipts and verify automatic database-only cutover. | | 4 | `SEC-P0-003` | In progress | Webhook trust and replay protection | Telegram secret-token verification code exists; production secret activation remains unproven. Exit: secret provisioned outside source, required mode enabled, invalid-secret 401 and authorized callback canary pass. | | 5 | `SUPPLY-P0-001` | In progress | Gitea-only secure software supply chain | Gitea-native checkout, secret-safe `.dockerignore`, commit-bound source receipt and governance gate are active. Exit: exact dependency lock, internal SAST/SCA/secret scan, SBOM, image digest/provenance, vulnerability SLA and production digest readback. | @@ -26,7 +26,7 @@ | 8 | `RAG-P0-001` | Not started | Internal RAG candidate canary | V10.770 stops at candidate preview. Exit: bounded pgvector candidate canary, signature/readback/feedback receipt, no price write and no premature `ai_insights` promotion. Next: `run_internal_rag_candidate_canary`. | | 9 | `MCP-P0-001` | In progress (`federation_source_ready`) | MCP/RAG production runtime closure | V10.796 source adds a strict public aggregate receipt for canonical `ewoooc` and `momo-pro-system` identities without opening authenticated internal APIs or exposing endpoint/tool payload data. Exit still requires V10.796 production `/health`, two fresh AWOOOI durable receipts with fingerprint recompute, live MCP servers/router/RAG, approved caller/tool boundary and production query canary. Current source readiness must not be reported as runtime closure. | | 10 | `SEC-P0-004` | Not started | Security operations lifecycle and metrics | Add durable security incident state and publish MTTA, MTTR, recurrence, false positive, human intervention, verifier pass, rollback and freshness. Exit: detect-to-learn production receipt. | -| 11 | `REL-P0-001` | In progress | Formal deploy and visible proof discipline | Gitea runtime source is main `01329c7048ffb54d7f323e84d8c9de086eb1cd68`. Action #1131 is waiting because no matching `ewoooc-host` runner is online, so it provides no CD execution evidence. A bounded exact-Git-object fallback deployed V10.799 from archive SHA-256 `b562f9eb929a75148825506bd381a74f58b87aab268b7120966bd80b3732059b`; check, deploy and independent post-verifier receipts are under `/home/ollama/momo-deploy-backups/ewoooc-20260715T023846Z-01329c7/`. Production `/health` is healthy at V10.799; all three runtime file hashes equal the source commit, three application containers are healthy, and `momo-db` remained unchanged at `cd092451cb5fd555d0ffff70642e109f3b742882c418beeab631793d1e9dc55d`. Production template and runtime data contract now yield the 51.0% revenue KPI and 21/50 product detail; no new authenticated browser screenshot is claimed because the available browser session was logged out. Next: restore the dedicated runner and require commit-bound automated CD/post-verifier plus authenticated desktop/mobile smoke for subsequent runtime changes. | +| 11 | `REL-P0-001` | In progress | Formal deploy and visible proof discipline | Gitea main is `4670f80f98f705050784f26adf7797b54e4c409f`; Action #1132 is waiting because no matching `ewoooc-host` runner is online and therefore provides no CD execution evidence. A bounded exact-Git-object fallback deployed V10.800 from archive SHA-256 `d660339f30727f62bf2f39eae2665fe6b832f77e6743743bc8ea85b6206dbc05`; check/apply/hash/preflight/canary receipts are under `/home/ollama/momo-deploy-backups/ewoooc-20260715T045049Z-4670f80/`. Production internal/external `/health` is healthy at V10.800, all 16 runtime hashes match the exact object, three application containers are healthy, and `momo-db` remained unchanged at `cd092451cb5fd555d0ffff70642e109f3b742882c418beeab631793d1e9dc55d`. Next: deploy the tested V10.801 promotion-policy fix through the same bounded path, then complete authenticated desktop/mobile visible smoke; runner recovery remains a release-governance gap. | ### GROWTH-P0-001 Fixed Execution Lanes @@ -36,7 +36,7 @@ These lanes are one ordered current P0, not optional side work. They must advanc |---|---|---|---| | A. Sales freshness | In progress (`sla_runtime_closed_source_redundancy_partial`) | Latest sales date `2026-07-13`; before the `2026-07-15 20:00` cutoff, raw lag is `2` but SLA lag is `0`, state is `grace` and decisions remain released. Scheduler receipt `0f24219e0bbb4740b7ad6645e7952296` and explicit canary receipt `6e2df008f65544fe8e557c11ff0dbb93` both persisted `completed_no_write`; durable decision is `no_candidate_fresh_no_write`. Live and persisted readiness now agree at Google Drive `1/4`; HTTPS, IMAP and local remain disabled. | Continue automatic report-arrival reconciliation. At/after 20:00 require `2026-07-14` or automatically block decision use, emit the bounded upstream action and verify the next arrival receipt; keep source redundancy partial until another approved source is live. | | B. Verified same-item evidence | In progress | TOP50 fixed cohort: `21` verified, `1` candidate/source validation, `28` unmatched. Count coverage is `42%`; revenue-weighted coverage is `NT$180,667 / NT$354,062 = 51.0%`. V10.797 outer-pack false exact candidates were quarantined without deletion; V10.798 normalizes nested quantity, and V10.799 prevents mapping status from changing cohort membership. Runs `58bb702057284cd382c82314b418d713` and `cb40a5dcdaa045c69bf9611ebb13bba2` wrote/read back five verified offers; run `14d4aac64d434e34bfbd0f0edb7ef1eb` closed no-write after zero safe candidates. | Wait for fresh source evidence before retrying the same unresolved subset, broaden approved structured platform adapters, preserve deterministic identity/unit/variant gates, and publish both count and revenue coverage deltas with the same scope fingerprint per run. | -| C. Platform runtime coverage | In progress (`source_candidate_ready`) | Production remains `1/15` registered external sources with formal runtime data (`6.7%`); PixelRAG remains evidence-only. V10.800 source adds the first additional structured adapter for Yahoo Shopping and has three current in-stock exact candidates, but no production activation or KPI gain is claimed yet. | Deploy through Gitea, run Yahoo check-mode then bounded execute, require at least two exact offers with independent DB readback before atomically enabling `active + enabled + write_enabled`, and roll back/quarantine same-run rows on any failed post-verifier. | +| C. Platform runtime coverage | In progress (`runtime_canary_no_write`) | V10.800 is live, scheduler registration is verified and two production runs persisted durable receipts. Limit 8 closed no-write; limit 20 found `3 exact + 1 unit-price`, and check-mode stopped the mixed batch before DB write. Formal runtime remains `1/15`; PixelRAG remains evidence-only. | Deploy V10.801 source-profile promotion partition, rerun limit 20, write/read back exact only, retain unit-price as candidate-only, then require at least two exact readbacks before atomically enabling `active + enabled + write_enabled`. | ## P1 @@ -47,10 +47,10 @@ These lanes are one ordered current P0, not optional side work. They must advanc | 14 | `APPSEC-P1-001` | In progress | CSP and DOM/XSS hardening | Security headers are present and CSP is report-only. Collect violations, remove high-risk `innerHTML`/inline sinks, then enforce CSP by canary. | | 15 | `APPSEC-P1-002` | Not started | Unsafe shared-cache serialization removal | Replace writable pickle caches in dashboard/daily-sales/EDM/sales with constrained JSON or signed schema. | | 16 | `ARCH-P1-001` | In progress | Split oversized policy/executor/verifier modules | Current top debts include 44k-line PChome mapping and 14k-line smoke service. Split by bounded family and independent tests. | -| 17 | `UX-P1-001` | In progress | Professional full-site UI/UX | V10.799 production template leads with `業績比價覆蓋`, now backed by 51.0% revenue coverage and 21/50 product transparency instead of the old unqualified 10% card. Mapping and automation evidence remain structured grids with progressive disclosure, and the first viewport distinguishes calendar lag from report-arrival SLA. Existing V10.795 desktop/mobile overflow smoke remains valid for layout; this release still needs a fresh authenticated production screenshot before visible closure is claimed. Continue the same first-viewport, progressive-disclosure, accessibility and state coverage across every remaining primary page. | +| 17 | `UX-P1-001` | In progress | Professional full-site UI/UX | V10.800 production template leads with `業績比價覆蓋`, backed by 51.0% revenue coverage and 21/50 product transparency instead of the old unqualified 10% card. External platform evidence now separates formal and candidate counts with compact chips and hidden detail text. Existing V10.795 desktop/mobile overflow smoke remains valid for layout; this release still needs a fresh authenticated production screenshot before visible closure is claimed. Continue the same first-viewport, progressive-disclosure, accessibility and state coverage across every remaining primary page. | | 18 | `PIXELRAG-P1-001` | Not started | Ollama-first multimodal embedding benchmark | Verify approved visual embedding on GCP-A -> GCP-B -> 111 and design pgvector-compatible visual metadata; FAISS remains disallowed without ADR. | -| 19 | `MARKET-P1-001` | In progress | Marketplace source contracts | Yahoo Shopping has a V10.800 source candidate with public-boundary allowlists, bounded timeout/bytes/rate, provenance, current product-detail readback, stock/spec/variant guards and controlled canary. Production activation remains pending; Shopee, Coupang, ETMall, Friday and Rakuten still require equivalent structured contracts, and blocked pages remain non-product data. | -| 20 | `QA-P1-001` | In progress | Deterministic test and CI governance | V10.800 workstation source baseline is `2,105 passed / 9 skipped / 0 failed`; the final focused Yahoo, source-readiness, same-item rollback and modularization scope is `62 passed`. This is source/test evidence only. V10.799 production exact-object hashes, external health and fixed cohort fingerprint remain the runtime baseline; Action #1131 remains waiting without runner execution, so CI parity and V10.800 deployment are not yet claimed. Next: reproduce the terminal through Gitea or bounded exact-object fallback, then run the production Yahoo canary and post-verifier. | +| 19 | `MARKET-P1-001` | In progress | Marketplace source contracts | Yahoo Shopping V10.800 is live with public-boundary allowlists, bounded streaming/rate, provenance, current product-detail readback, stock/spec/variant guards and controlled canary. Production activation remains pending because mixed exact/unit promotion was safely blocked; V10.801 adds source-specific promotion partition. Shopee, Coupang, ETMall, Friday and Rakuten still require equivalent structured contracts, and blocked pages remain non-product data. | +| 20 | `QA-P1-001` | In progress | Deterministic test and CI governance | V10.801 workstation source baseline is `2,106 passed / 9 skipped / 0 failed`; focused promotion-policy/Yahoo/source-readiness/same-item scope is `63 passed`. V10.800 production exact-object hashes, health, scheduler registration, two no-write receipts and fixed cohort fingerprint are verified. Action #1132 remains waiting without runner execution, so CI parity is not claimed. Next: deploy V10.801 and require exact DB readback plus source activation receipt. | ## P2 diff --git a/services/pchome_growth_momo_backfill_service.py b/services/pchome_growth_momo_backfill_service.py index ba1e1ea..745851a 100644 --- a/services/pchome_growth_momo_backfill_service.py +++ b/services/pchome_growth_momo_backfill_service.py @@ -103,7 +103,7 @@ def run_pchome_growth_momo_backfill( 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, readback_func: Callable[[Any, list[dict[str, Any]]], dict[str, Any]] | None = None, - source_profile: dict[str, str] | None = None, + source_profile: dict[str, Any] | None = None, source_activation_func: Callable[..., dict[str, Any]] | None = None, source_rollback_func: Callable[..., dict[str, Any]] | None = None, ) -> dict[str, Any]: @@ -121,6 +121,12 @@ def run_pchome_growth_momo_backfill( **(source_profile or {}), } source_label = str(profile.get("display_name") or "外部平台") + formal_compare_types = { + str(item) + for item in ( + profile.get("formal_compare_types") or ("total_price", "unit_price") + ) + } identity = new_reconciliation_identity( str(profile.get("work_item_id") or "GROWTH-P0-001-B") ) @@ -172,6 +178,24 @@ def run_pchome_growth_momo_backfill( exact_candidates = [item for item in auto_candidates if candidate_auto_compare_type(item) == "total_price"] unit_candidates = [item for item in auto_candidates if candidate_auto_compare_type(item) == "unit_price"] review_candidates = list(candidate_verification.get("review_candidates") or []) + promotion_policy_deferred = [] + for candidate in [*exact_candidates, *unit_candidates]: + compare_type = candidate_auto_compare_type(candidate) + if compare_type in formal_compare_types: + continue + deferred = dict(candidate) + deferred["promotion_policy_status"] = "candidate_only" + deferred["promotion_policy_reason"] = ( + "source_profile_compare_type_not_allowed" + ) + promotion_policy_deferred.append(deferred) + review_candidates.extend(promotion_policy_deferred) + exact_candidates = [ + item for item in exact_candidates if "total_price" in formal_compare_types + ] + unit_candidates = [ + item for item in unit_candidates if "unit_price" in formal_compare_types + ] auto_candidates = [*exact_candidates, *unit_candidates] external_offer_sync = { "success": True, @@ -324,11 +348,13 @@ def run_pchome_growth_momo_backfill( "formal_price_write_requires_independent_verifier": True, "pixelrag_direct_price_write_allowed": False, "source_activation_requires_canary_readback": bool(source_activation_func), + "formal_compare_types": sorted(formal_compare_types), }, "check_mode": { "receipt_schema_ready": bool(schema_receipt.get("success")), "verified_candidate_count": len(auto_candidates), "blocked_candidate_count": int(candidate_verification.get("blocked_candidate_count") or 0), + "promotion_policy_deferred_count": len(promotion_policy_deferred), }, "execution": { "idempotent_bounded_execution": True, @@ -395,6 +421,7 @@ def run_pchome_growth_momo_backfill( "unit_compare_count": len(unit_candidates), "auto_compare_count": len(auto_candidates), "review_count": len(review_candidates), + "promotion_policy_deferred_count": len(promotion_policy_deferred), "external_offer_sync": external_offer_sync, "review_candidate_sync": review_candidate_sync, "source_activation": source_activation, diff --git a/services/yahoo_shopping_public_offer_service.py b/services/yahoo_shopping_public_offer_service.py index 169d4e8..82d12b9 100644 --- a/services/yahoo_shopping_public_offer_service.py +++ b/services/yahoo_shopping_public_offer_service.py @@ -281,6 +281,7 @@ def run_pchome_growth_yahoo_backfill( "display_name": SOURCE_LABEL, "receipt_source": CONNECTOR_KEY, "work_item_id": WORK_ITEM_ID, + "formal_compare_types": ["total_price"], }, source_activation_func=activate, source_rollback_func=rollback, diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py index 947bfce..89b11b5 100644 --- a/tests/test_pchome_revenue_growth_service.py +++ b/tests/test_pchome_revenue_growth_service.py @@ -746,6 +746,104 @@ def test_backfill_rolls_back_failed_write_before_source_activation(monkeypatch): assert rollback_calls[0]["activation_receipt"]["applied"] is False +def test_backfill_source_profile_defers_disallowed_unit_price(monkeypatch): + from services import pchome_growth_momo_backfill_service as service + + engine = create_engine("sqlite:///:memory:") + exact = { + "product_id": "Y-EXACT", + "target_pchome_product_id": "PCH-1", + "auto_compare_type": "total_price", + } + unit = { + "product_id": "Y-UNIT", + "target_pchome_product_id": "PCH-2", + "auto_compare_type": "unit_price", + } + payload = { + "success": True, + "stats": {"candidate_validation_count": 2, "unmatched_count": 0}, + "opportunities": [ + { + "pchome_product_id": "PCH-1", + "product_name": "Exact 商品", + "pchome_price": 900, + "sales_7d": 1000, + "external_price": None, + "recommended_action": {"code": "map_external_product"}, + } + ], + } + captured = {} + + monkeypatch.setattr( + service, + "ensure_evidence_receipt_table", + lambda _engine: {"success": True}, + ) + monkeypatch.setattr( + service, + "verify_same_item_candidates", + lambda _targets, _candidates, *, identity: { + "verified_candidates": [exact, unit], + "review_candidates": [], + "blocked_candidate_count": 0, + "identity": identity, + }, + ) + monkeypatch.setattr( + service, + "build_coverage_post_verifier", + lambda **_kwargs: { + "all_checks_passed": True, + "status": "verified", + "readback_pass_count": 1, + "count_coverage_delta": 0, + "revenue_coverage_delta": 0, + "after": {"candidate_validation_count": 1, "unmatched_count": 0}, + }, + ) + monkeypatch.setattr( + service, + "persist_reconciliation_receipts", + lambda *_args, **_kwargs: {"success": True, "readback_count": 1}, + ) + + def sync(_engine, candidates): + captured["formal"] = candidates + return {"success": True, "status": "synced", "written_count": 1} + + def sync_review(_engine, candidates): + captured["candidate_only"] = candidates + return {"success": True, "status": "artifact_candidate_only", "written_count": 0} + + result = service.run_pchome_growth_momo_backfill( + engine, + limit=1, + build_payload_func=lambda _engine, _limit: payload, + search_func=lambda _targets, _limit: (True, "found", [exact, unit]), + sync_func=sync, + sync_review_func=sync_review, + readback_func=lambda _engine, _candidates: { + "success": True, + "readback_pass_count": 1, + }, + source_profile={ + "work_item_id": "GROWTH-P0-001-C", + "formal_compare_types": ["total_price"], + }, + ) + + assert result["success"] is True + assert [item["product_id"] for item in captured["formal"]] == ["Y-EXACT"] + assert [item["product_id"] for item in captured["candidate_only"]] == ["Y-UNIT"] + assert captured["candidate_only"][0]["promotion_policy_status"] == "candidate_only" + assert result["data"]["auto_compare_count"] == 1 + assert result["data"]["unit_compare_count"] == 0 + assert result["data"]["review_count"] == 1 + assert result["data"]["promotion_policy_deferred_count"] == 1 + + def test_pchome_growth_momo_backfill_default_search_uses_deeper_terms(monkeypatch): from services import pchome_growth_momo_backfill_service as service