Skip to content

Commit c85a5d5

Browse files
committed
Add purchase integrity health checks
1 parent 75b9d2c commit c85a5d5

9 files changed

Lines changed: 183 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: ci-${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
test:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 10
21+
steps:
22+
- name: Check out repository
23+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
27+
with:
28+
python-version: "3.12"
29+
cache: pip
30+
cache-dependency-path: dtc-growth-data-report/scripts/template/requirements.txt
31+
32+
- name: Install dependencies
33+
run: python -m pip install -r dtc-growth-data-report/scripts/template/requirements.txt
34+
35+
- name: Run regression tests
36+
run: python -m unittest discover -s tests -p 'test_*.py'

README.en.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This skill helps Codex set up and customize a local data pipeline for ecommerce
1616
- Weekly comparison report generator with current week vs previous week.
1717
- Operator-ready weekly report modules: business conclusions, revenue bridge, funnel health, Google Ads budget actions, page actions, SEO intent clusters, anomaly alerts, next-week owners, and data health checks.
1818
- Weekly funnel comparisons use dated GA4 add-to-cart and checkout events instead of a 90-day proxy total.
19+
- Purchase-integrity health compares aligned Shopify orders/revenue with GA4 purchases/revenue, escalates count gaps to transaction-level BigQuery reconciliation, and never auto-sends recovery events.
1920
- Boss-facing HTML dashboard generator.
2021
- Configuration and data contract references.
2122

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- 数据统一转换:输出标准 processed CSV。
2121
- 周报模板:自动生成“本周 vs 上周”的老板版 HTML 和 Markdown 周报。
2222
- 操盘手增强周报:包含经营判断、收入归因、漏斗健康、广告预算动作、页面优化动作、SEO 意图分组、异常提醒、下周行动清单和数据健康检查。
23+
- Purchase 完整性告警:对比同一周 Shopify 订单/收入与 GA4 purchase/收入;出现单量差异时置顶提示并要求 BigQuery `transaction_id` 逐单核验,本技能不会自动补发事件。
2324
- 周报漏斗按本周与上周分别统计加购和开始结账,不再用 90 天事件总量代替周度数据。
2425
- 独立站增长诊断看板:输出老板可读的本地 HTML dashboard。
2526
- 配置参考、数据字段说明和报告工作流说明。

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- 数据统一转换:输出标准 processed CSV。
2121
- 周报模板:自动生成“本周 vs 上周”的老板版 HTML 和 Markdown 周报。
2222
- 操盘手增强周报:包含经营判断、收入归因、漏斗健康、广告预算动作、页面优化动作、SEO 意图分组、异常提醒、下周行动清单和数据健康检查。
23+
- Purchase 完整性告警:对比同一周 Shopify 订单/收入与 GA4 purchase/收入;出现单量差异时置顶提示并要求 BigQuery `transaction_id` 逐单核验,本技能不会自动补发事件。
2324
- 周报漏斗按本周与上周分别统计加购和开始结账,不再用 90 天事件总量代替周度数据。
2425
- 独立站增长诊断看板:输出老板可读的本地 HTML dashboard。
2526
- 配置参考、数据字段说明和报告工作流说明。

dtc-growth-data-report/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ When customizing reports:
7272

7373
- Use Shopify as the default source of truth for revenue and orders.
7474
- Use GA4 for sessions, engagement, landing page behavior, and funnel events.
75+
- Compare aligned-period Shopify orders/revenue with GA4 `ecommercePurchases`/`totalRevenue` as a purchase-integrity health signal. Persist the count gap, revenue gap, and tracking rate in weekly JSON and show the result in data health.
76+
- Treat an aggregate gap as an alert, not proof of a specific missing order. Require BigQuery `transaction_id` versus Shopify paid-order reconciliation before naming affected orders or handing recovery to `$ga4-data-analysis`.
77+
- Keep this reporting skill read-only. Never send Measurement Protocol events or repair tracking automatically.
7578
- Fetch GA4 `add_to_cart` and `begin_checkout` with the `date` dimension so weekly funnel stages use the same current and previous periods as the rest of the report.
7679
- Use Google Ads for spend, clicks, conversions, conversion value, ROAS, CPA, search terms, and ad landing URLs.
7780
- Use campaign-level daily Google Ads facts for totals and weekly campaign decisions so Performance Max is included; keep ad-group data only for compatible campaign detail.
@@ -96,6 +99,7 @@ Before final handoff:
9699
- Confirm HTML/Markdown reports exist and are non-empty.
97100
- Confirm conversion rates and CTR are percentages, not decimals.
98101
- Confirm GA4 funnel rows compare dated `add_to_cart` and `begin_checkout` values for the current and previous aligned weeks.
102+
- Confirm the report compares Shopify orders with GA4 `ecommercePurchases`, leads with the risk when counts differ, and explicitly requires transaction-level reconciliation before recovery.
99103
- Confirm Google Ads cost is in account currency units, not micros.
100104
- Confirm campaign-level Google Ads coverage includes Performance Max and that account totals are not derived only from `ad_group` rows.
101105
- Confirm sitewide ROI equals Shopify revenue divided by Google Ads spend, and GSC clicks appear in the core KPI table.

dtc-growth-data-report/references/data-contract.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ The transform step should produce:
5757
- SEO impressions, clicks, CTR, average position: GSC.
5858
- Store conversion rate: Shopify orders divided by GA4 sessions.
5959
- Sitewide ROI: Shopify revenue divided by total Google Ads cost for the same aligned period.
60+
- Purchase tracking rate: GA4 `ecommercePurchases` divided by Shopify orders for the same aligned period. When both are zero, report 100% coverage rather than a divide-by-zero error.
61+
- Purchase count gap: Shopify orders minus GA4 `ecommercePurchases`. Purchase revenue gap: Shopify revenue minus GA4 `totalRevenue`. These aggregate fields are health signals only; exact missing orders require Shopify paid-order IDs versus BigQuery `transaction_id` reconciliation.
6062
- Add-to-cart rate: GA4 `add_to_cart` events divided by GA4 sessions for the same report period.
6163
- Cart-to-checkout rate: GA4 `begin_checkout` events divided by GA4 `add_to_cart` events for the same report period.
6264
- ROAS: Google Ads conversion value divided by Google Ads cost.

dtc-growth-data-report/references/report-workflows.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Required sections:
2626
- Top KPI cards: Shopify revenue, orders, store conversion rate, sitewide ROI (`Shopify revenue / Google Ads spend`), and Google Ads ROAS.
2727
- Revenue bridge: split revenue movement into traffic, conversion rate, and AOV effects.
2828
- Funnel health: show GA4 Sessions, dated `add_to_cart`, dated `begin_checkout`, Shopify purchases, and ad click-to-conversion efficiency. Compare GA4 funnel events for the current and previous aligned weeks; do not substitute a 90-day event total when dated data is available.
29+
- Data health: compare Shopify orders/revenue with GA4 `ecommercePurchases`/`totalRevenue` for the same aligned week. Put a count gap in the Executive Summary and P0 actions, but do not identify or repair orders until BigQuery `transaction_id` reconciliation confirms them. Hand authorized recovery to `$ga4-data-analysis`; this report remains read-only.
2930
- Daily Google Ads spend and weekly spend total
3031
- Core KPI table: current week, previous week, and change, including GSC clicks alongside impressions and CTR.
3132
- Channel traffic changes

dtc-growth-data-report/scripts/template/generate_weekly_comparison_template.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ def summarize_period(
185185
gsc_week = period_filter(gsc, "parsed_date", start, end)
186186

187187
sessions = metric_sum(ga4_week, "sessions")
188+
ga4_purchases = metric_sum(ga4_week, "ecommercePurchases")
189+
ga4_purchase_revenue = metric_sum(ga4_week, "totalRevenue")
188190
orders = metric_sum(shopify_week, "orders")
189191
revenue = metric_sum(shopify_week, "total_sales")
190192
ad_spend = metric_sum(ads_week, "cost")
@@ -197,6 +199,11 @@ def summarize_period(
197199
return {
198200
"revenue": revenue,
199201
"orders": orders,
202+
"ga4_purchases": ga4_purchases,
203+
"ga4_purchase_revenue": ga4_purchase_revenue,
204+
"purchase_count_gap": orders - ga4_purchases,
205+
"purchase_revenue_gap": revenue - ga4_purchase_revenue,
206+
"purchase_tracking_rate": safe_divide(ga4_purchases, orders) if orders else (1.0 if ga4_purchases == 0 else 0.0),
200207
"sessions": sessions,
201208
"conversion_rate": safe_divide(orders, sessions),
202209
"aov": safe_divide(revenue, orders),
@@ -635,7 +642,19 @@ def anomaly_rows(current: dict[str, float], previous: dict[str, float]) -> list[
635642

636643
def data_health_rows(current: dict[str, float], coverage: dict[str, Any], funnel: list[dict[str, str]]) -> list[dict[str, str]]:
637644
ads_vs_shopify_gap = current["ad_value"] - current["revenue"]
645+
purchase_count_gap = current.get("purchase_count_gap", 0.0)
646+
ga4_purchases = current.get("ga4_purchases", 0.0)
647+
ga4_purchase_revenue = current.get("ga4_purchase_revenue", 0.0)
638648
has_dated_funnel = any(row.get("metric") == "GA4 开始结账" for row in funnel)
649+
if purchase_count_gap > 0:
650+
purchase_status = "高风险"
651+
purchase_gap_text = f"GA4 比 Shopify 少 {number(purchase_count_gap)} 单"
652+
elif purchase_count_gap < 0:
653+
purchase_status = "异常"
654+
purchase_gap_text = f"GA4 比 Shopify 多 {number(abs(purchase_count_gap))} 单"
655+
else:
656+
purchase_status = "通过"
657+
purchase_gap_text = "订单数一致"
639658
rows = [
640659
{
641660
"check": "四源周期",
@@ -647,6 +666,15 @@ def data_health_rows(current: dict[str, float], coverage: dict[str, Any], funnel
647666
"status": "通过" if current["orders"] >= 0 else "异常",
648667
"detail": f"本周 Shopify 订单 {number(current['orders'])},收入 {money(current['revenue'])}。",
649668
},
669+
{
670+
"check": "Shopify vs GA4 purchase",
671+
"status": purchase_status,
672+
"detail": (
673+
f"Shopify {number(current['orders'])} 单 / {money(current['revenue'])};"
674+
f"GA4 purchase {number(ga4_purchases)} 单 / {money(ga4_purchase_revenue)};"
675+
f"{purchase_gap_text}。聚合差异只用于告警,需用 BigQuery transaction_id 对账后确认。"
676+
),
677+
},
650678
{
651679
"check": "GA4 周度漏斗",
652680
"status": "通过" if has_dated_funnel else "待修复",
@@ -669,6 +697,14 @@ def next_action_rows(data: dict[str, Any]) -> list[dict[str, str]]:
669697
top_page = pages[0] if pages else None
670698
top_seo = seo[0] if seo else None
671699
rows = []
700+
if data.get("current", {}).get("purchase_count_gap", 0) > 0:
701+
rows.append({
702+
"priority": "P0",
703+
"task": "核对 Shopify paid order 与 GA4 BigQuery transaction_id",
704+
"owner": "数据/追踪",
705+
"target": "确认 GA4 purchase 漏记数量和收入影响",
706+
"done": "完成逐单核验;不在本报告流程自动补发,需交给受控 GA4 recovery 流程。",
707+
})
672708
if top_budget:
673709
rows.append({"priority": "P0", "task": f"{top_budget['action']}{top_budget['campaign']} / {top_budget['ad_group']}", "owner": "投放", "target": "降低无效花费,观察 CPA/ROAS", "done": "预算动作完成并在 3 天后复盘。"})
674710
if top_page:
@@ -797,12 +833,18 @@ def build_summary(data: dict[str, Any]) -> list[str]:
797833
roas_delta = delta_value(current["roas"], previous["roas"])["display"]
798834
seo_ctr_delta = pp_delta(current["seo_ctr"], previous["seo_ctr"])
799835
top_action = data["next_actions"][0]["task"] if data.get("next_actions") else "先处理最大经营杠杆。"
800-
return [
836+
summary = [
801837
f"本周 Shopify 收入 {money(current['revenue'])},订单 {number(current['orders'])},较上周收入变化 {revenue_delta};转化率 {pct(current['conversion_rate'])},较上周 {cvr_delta}。",
802838
f"Google Ads 本周花费 {money(current['ad_spend'])},ROAS {current['roas']:.2f},较上周 {roas_delta};广告转化价值 {money(current['ad_value'])}。",
803839
f"GSC 本周曝光 {number(current['seo_impressions'])}、点击 {number(current['seo_clicks'])},CTR {pct(current['seo_ctr'])},较上周 {seo_ctr_delta}。",
804840
f"下周第一动作:{top_action}",
805841
]
842+
if current.get("purchase_count_gap", 0) != 0:
843+
summary.insert(
844+
0,
845+
f"数据风险:Shopify {number(current['orders'])} 单,GA4 purchase {number(current['ga4_purchases'])} 单;先按 BigQuery transaction_id 逐单核验,再做渠道或预算判断。",
846+
)
847+
return summary
806848

807849

808850
def build_markdown(data: dict[str, Any]) -> str:

tests/test_purchase_integrity.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
import unittest
5+
from datetime import date
6+
from pathlib import Path
7+
8+
import pandas as pd
9+
10+
11+
TEMPLATE_DIR = Path(__file__).resolve().parents[1] / "dtc-growth-data-report" / "scripts" / "template"
12+
sys.path.insert(0, str(TEMPLATE_DIR))
13+
14+
import generate_weekly_comparison_template as weekly # noqa: E402
15+
16+
17+
class PurchaseIntegrityTests(unittest.TestCase):
18+
def source_frames(self, *, shopify_orders: int, ga4_purchases: int):
19+
report_date = pd.Timestamp("2026-08-16")
20+
ga4 = pd.DataFrame(
21+
[
22+
{
23+
"parsed_date": report_date,
24+
"sessions": 100,
25+
"ecommercePurchases": ga4_purchases,
26+
"totalRevenue": ga4_purchases * 100,
27+
}
28+
]
29+
)
30+
shopify = pd.DataFrame(
31+
[
32+
{
33+
"parsed_date": report_date,
34+
"orders": shopify_orders,
35+
"total_sales": shopify_orders * 100,
36+
}
37+
]
38+
)
39+
ads = pd.DataFrame([{"parsed_date": report_date}])
40+
gsc = pd.DataFrame([{"parsed_date": report_date}])
41+
return ga4, shopify, ads, gsc
42+
43+
def test_period_summary_persists_shopify_vs_ga4_purchase_gap(self) -> None:
44+
summary = weekly.summarize_period(
45+
*self.source_frames(shopify_orders=3, ga4_purchases=2),
46+
date(2026, 8, 16),
47+
date(2026, 8, 16),
48+
)
49+
50+
self.assertEqual(summary["ga4_purchases"], 2)
51+
self.assertEqual(summary["purchase_count_gap"], 1)
52+
self.assertEqual(summary["purchase_tracking_rate"], 2 / 3)
53+
self.assertEqual(summary["purchase_revenue_gap"], 100)
54+
55+
def test_data_health_flags_missing_ga4_purchases_without_auto_recovery(self) -> None:
56+
summary = weekly.summarize_period(
57+
*self.source_frames(shopify_orders=3, ga4_purchases=2),
58+
date(2026, 8, 16),
59+
date(2026, 8, 16),
60+
)
61+
health = weekly.data_health_rows(
62+
summary,
63+
{"sources": {"GSC": {"last_date": "2026-08-16"}}},
64+
[{"metric": "GA4 开始结账"}],
65+
)
66+
integrity = next(row for row in health if row["check"] == "Shopify vs GA4 purchase")
67+
68+
self.assertEqual(integrity["status"], "高风险")
69+
self.assertIn("少 1 单", integrity["detail"])
70+
self.assertIn("BigQuery transaction_id", integrity["detail"])
71+
72+
actions = weekly.next_action_rows({"current": summary, "funnel": []})
73+
self.assertEqual(actions[0]["priority"], "P0")
74+
self.assertIn("核对 Shopify paid order", actions[0]["task"])
75+
self.assertIn("不在本报告流程自动补发", actions[0]["done"])
76+
77+
def test_matching_zero_order_week_is_healthy(self) -> None:
78+
summary = weekly.summarize_period(
79+
*self.source_frames(shopify_orders=0, ga4_purchases=0),
80+
date(2026, 8, 16),
81+
date(2026, 8, 16),
82+
)
83+
health = weekly.data_health_rows(
84+
summary,
85+
{"sources": {"GSC": {"last_date": "2026-08-16"}}},
86+
[{"metric": "GA4 开始结账"}],
87+
)
88+
integrity = next(row for row in health if row["check"] == "Shopify vs GA4 purchase")
89+
self.assertEqual(summary["purchase_tracking_rate"], 1.0)
90+
self.assertEqual(integrity["status"], "通过")
91+
92+
93+
if __name__ == "__main__":
94+
unittest.main()

0 commit comments

Comments
 (0)