@1aboveio/skills 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +298 -216
- package/README.md +3 -3
- package/package.json +10 -8
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/SKILL.md +35 -37
- package/skills/app-dev/android-foundation/references/delivery-focus.md +4 -2
- package/skills/app-dev/android-foundation/references/feature-work.md +50 -0
- package/skills/app-dev/android-foundation/references/setup.md +24 -9
- package/skills/app-dev/android-foundation/references/workspace.md +81 -0
- package/skills/app-dev/android-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
- package/skills/app-dev/android-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
- package/skills/app-dev/android-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +15 -10
- package/skills/app-dev/ios-foundation/references/setup.md +25 -9
- package/skills/app-dev/ios-foundation/references/workspace.md +82 -0
- package/skills/app-dev/ios-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
- package/skills/app-dev/ios-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
- package/skills/app-dev/ios-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +3 -3
- package/skills/core/distill-writing/references/extraction.md +1 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +82 -12
- package/skills/core/editorial-review/references/checklist.md +26 -1
- package/skills/core/house-style/SKILL.md +8 -3
- package/skills/core/house-style/references/analytical-reporting.md +123 -0
- package/skills/core/house-style/references/analytical-writing-patterns.md +79 -0
- package/skills/core/house-style/references/analytical-writing-research.md +129 -0
- package/skills/core/{write-doc → house-style}/references/bilingual.md +7 -5
- package/skills/core/house-style/references/definitions-and-metrics.md +86 -0
- package/skills/core/house-style/references/document-workflow.md +193 -0
- package/skills/core/house-style/references/initialization.md +66 -0
- package/skills/core/house-style/references/persistence.md +9 -4
- package/skills/core/house-style/references/rules.md +11 -4
- package/skills/core/house-style/scripts/init-guidance.mjs +213 -0
- package/skills/core/house-style/templates/analytical-report.md +54 -0
- package/skills/core/house-style/templates/gap-analysis.md +1 -1
- package/skills/core/plain-language/SKILL.md +2 -2
- package/skills/core/polish-cn/SKILL.md +9 -2
- package/skills/core/reg-parser/SKILL.md +3 -3
- package/skills/core/terminology/SKILL.md +8 -4
- package/skills/core/write-doc/SKILL.md +40 -151
- package/skills/core/write-report/SKILL.md +129 -0
- package/skills/core/{write-doc → write-report}/references/compliance-reports.md +4 -4
- package/skills/engineering/better-review/SKILL.md +37 -37
- package/skills/engineering/better-review/references/passes.md +35 -41
- package/skills/engineering/better-review/references/result.md +10 -9
- package/skills/engineering/e2e-test/SKILL.md +17 -17
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +11 -13
- package/skills/engineering/e2e-test/references/presentation-sweep.md +7 -7
- package/skills/engineering/e2e-test/references/reviewing.md +9 -11
- package/skills/engineering/e2e-test/references/route-manifest.md +1 -1
- package/skills/engineering/engineering/SKILL.md +201 -0
- package/skills/engineering/engineering/references/engineering-conventions.md +32 -0
- package/skills/engineering/engineering/references/feature-delivery.md +8 -0
- package/skills/engineering/engineering/references/init-scope.md +45 -0
- package/skills/engineering/engineering/references/scope-management.md +85 -0
- package/skills/engineering/engineering/scripts/init.mjs +160 -0
- package/skills/engineering/{contributing → engineering}/scripts/persist-conventions.mjs +1 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +19 -19
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/engineering/ensure-coverage/SKILL.md +22 -109
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +7 -5
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +4 -4
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +3 -3
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +4 -65
- package/skills/engineering/ensure-coverage/references/depth/grading.md +5 -7
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +6 -12
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +1 -1
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +6 -6
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +7 -23
- package/skills/engineering/ensure-coverage/references/process/output-template.md +18 -133
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +7 -12
- package/skills/engineering/ensure-coverage/scripts/README.md +12 -4
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +3 -1
- package/skills/engineering/harness-runtime/bin/workspaces.mjs +52 -0
- package/skills/engineering/harness-runtime/native-workspace.mjs +239 -0
- package/skills/engineering/harness-runtime/references/workspaces.md +78 -0
- package/skills/engineering/harness-runtime/workspace-lifecycle.mjs +2154 -0
- package/skills/engineering/resolve-issues/SKILL.md +40 -61
- package/skills/engineering/resolve-issues/references/ci.md +13 -3
- package/skills/engineering/resolve-issues/references/delivery-progress.md +4 -3
- package/skills/engineering/resolve-issues/references/done.md +11 -2
- package/skills/engineering/resolve-issues/references/expire.md +9 -2
- package/skills/engineering/resolve-issues/references/implementation.md +33 -17
- package/skills/engineering/resolve-issues/references/preflight.md +3 -1
- package/skills/engineering/resolve-issues/references/principles.md +52 -0
- package/skills/engineering/resolve-issues/references/review.md +25 -15
- package/skills/engineering/resolve-issues/references/task-plan.md +64 -27
- package/skills/engineering/resolve-issues/references/workspaces.md +164 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +185 -317
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +44 -7
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +36 -2
- package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +9 -8
- package/skills/engineering/resolve-issues/scripts/workspace-lifecycle.mjs +1 -0
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +7 -5
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +19 -4
- package/skills/engineering/test-quality/references/lint.md +1 -1
- package/skills/engineering/test-quality/references/principles.md +2 -2
- package/skills/engineering/test-quality/references/testing-conventions.md +1 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +4 -8
- package/skills/risk-compliance/compliance-audit/references/audience-and-reports.md +12 -0
- package/skills/risk-compliance/contract-compare/SKILL.md +17 -1
- package/skills/risk-compliance/contract-review/SKILL.md +6 -1
- package/skills/risk-compliance/contract-review/references/report-stage.md +17 -0
- package/skills/risk-compliance/doc-authenticity/SKILL.md +11 -1
- package/skills/risk-compliance/fraud-detection/LICENSE +3 -0
- package/skills/risk-compliance/fraud-detection/SKILL.md +148 -0
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/first-party-fraud.md +3 -2
- package/skills/risk-compliance/fraud-detection/references/archetypes/third-party-fraud.md +47 -0
- package/skills/risk-compliance/fraud-detection/references/collection-payout-features.md +151 -0
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/contexts/bank-transfer.md +4 -3
- package/skills/risk-compliance/fraud-detection/references/contexts/card-payment.md +70 -0
- package/skills/risk-compliance/fraud-detection/references/contexts/payment-collection.md +34 -0
- package/skills/risk-compliance/fraud-detection/references/contexts/payout.md +34 -0
- package/skills/risk-compliance/fraud-detection/references/enrichment.md +74 -0
- package/skills/risk-compliance/fraud-detection/references/entities.md +48 -0
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/feature-engineering.md +69 -33
- package/skills/risk-compliance/fraud-detection/references/feature-sql.md +150 -0
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-rationale.md +4 -2
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-template.md +45 -23
- package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/review-checklist.md +60 -20
- package/skills/risk-compliance/fraud-detection/references/workflow.md +216 -0
- package/skills/risk-compliance/fraud-detection/release-check.mjs +95 -0
- package/skills/risk-compliance/payment-analysis/SKILL.md +36 -17
- package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +2 -2
- package/skills/risk-compliance/payment-analysis/references/report-template.md +10 -2
- package/skills/risk-compliance/payment-analysis/references/visualization.md +11 -1
- package/skills/risk-compliance/portfolio-payment-analysis/LICENSE +3 -0
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +170 -0
- package/skills/risk-compliance/portfolio-payment-analysis/references/identity-and-reconciliation.md +46 -0
- package/skills/risk-compliance/portfolio-payment-analysis/references/metrics-and-reporting.md +170 -0
- package/skills/risk-compliance/portfolio-payment-analysis/references/risk-history.md +89 -0
- package/skills/risk-compliance/portfolio-payment-analysis/release-check.mjs +95 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/analyze.py +636 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/build_report.py +386 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/comparison_charts.py +425 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/csv_export.py +52 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/render.py +160 -0
- package/skills/risk-compliance/portfolio-payment-analysis/scripts/risk_history.py +435 -0
- package/skills/risk-compliance/similar-domain/SKILL.md +9 -4
- package/skills/risk-compliance/tld-detection/SKILL.md +124 -239
- package/skills/risk-compliance/tld-detection/agents/openai.yaml +1 -1
- package/skills/risk-compliance/tld-detection/references/clean.md +18 -51
- package/skills/risk-compliance/tld-detection/references/fx-conversion.md +56 -219
- package/skills/risk-compliance/tld-detection/references/metric-contract.md +109 -0
- package/skills/risk-compliance/tld-detection/references/pattern.md +99 -0
- package/skills/risk-compliance/tld-detection/references/report-template.md +49 -272
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +46 -13
- package/skills/risk-compliance/tld-detection/scripts/detect_patterns.py +346 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +51 -19
- package/skills/risk-compliance/tld-detection/scripts/profile_groups.py +109 -0
- package/skills/risk-compliance/tld-detection/scripts/render_charts.py +218 -0
- package/skills/risk-compliance/tld-detection/scripts/tld_common.py +243 -0
- package/skills/engineering/contributing/SKILL.md +0 -70
- package/skills/risk-compliance/fraud-analysis/SKILL.md +0 -114
- package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +0 -27
- package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +0 -30
- package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +0 -20
- package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +0 -20
- package/skills/risk-compliance/fraud-analysis/references/workflow.md +0 -175
- package/skills/risk-compliance/tld-detection/references/chinese-polish.md +0 -55
- package/skills/risk-compliance/tld-detection/references/clustering.md +0 -235
- package/skills/risk-compliance/tld-detection/references/decision.md +0 -189
- package/skills/risk-compliance/tld-detection/references/streams.md +0 -141
- package/skills/risk-compliance/tld-detection/references/terminology.md +0 -95
- package/skills/risk-compliance/tld-detection/references/workflow.md +0 -95
- /package/skills/{engineering/contributing → core/write-report}/LICENSE +0 -0
- /package/skills/core/{write-doc → write-report}/references/reports/audit-report-template.md +0 -0
- /package/skills/core/{write-doc → write-report}/references/reports/external-assessment-template.md +0 -0
- /package/skills/core/{write-doc → write-report}/references/reports/management-report-template.md +0 -0
- /package/skills/core/{write-doc → write-report}/references/reports/vars-report-template.md +0 -0
- /package/skills/{engineering/contributing → core/write-report}/release-check.mjs +0 -0
- /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/LICENSE +0 -0
- /package/skills/engineering/{contributing → engineering}/references/tracker-conventions.md +0 -0
- /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/release-check.mjs +0 -0
- /package/skills/engineering/{contributing → engineering}/scripts/persist-conventions-core.mjs +0 -0
- /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/authorized-payment-scam.md +0 -0
- /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/mechanisms/account-takeover.md +0 -0
- /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/taxonomy.md +0 -0
- /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/terminology.md +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a submerchant-only Chinese report from analyze.py facts.
|
|
3
|
+
|
|
4
|
+
Usage: python3 build_report.py --facts OUTPUT/facts.json --output OUTPUT
|
|
5
|
+
Portfolio fields remain calculation/reconciliation evidence; this report never renders them.
|
|
6
|
+
Edit presentation.json for narrative review, then rerender with render.py.
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
from csv_export import safe_writer
|
|
10
|
+
import json
|
|
11
|
+
from datetime import date
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from render import render
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def number(value):
|
|
18
|
+
return '' if value is None else f'{value:,.0f}'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def money(value):
|
|
22
|
+
return '' if value is None else f'{Decimal(str(value)):,.2f}'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def percent(value):
|
|
26
|
+
return '' if value is None else f'{value*100:.2f}%'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def name(merchant):
|
|
30
|
+
if not merchant.get('id'):
|
|
31
|
+
return '未归属'
|
|
32
|
+
return merchant.get('name', merchant['id']).removeprefix('HLP*')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def amount(merchant, kind, magnitude=False):
|
|
36
|
+
value=(merchant.get(kind) or {}).get('usd')
|
|
37
|
+
return money(abs(Decimal(value)) if value is not None and magnitude else value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def para(text, kind='paragraph'):
|
|
41
|
+
return {'type':kind,'text':text}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def table(headers, rows, **extra):
|
|
45
|
+
return {'type':'table','headers':headers,'rows':rows,**extra}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def top_reasons(merchant):
|
|
49
|
+
return sorted(merchant.get('decline_reasons',[]),key=lambda r:(-r['count'],r['code'],r['message']))[:5]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def weekly_blocks(facts, merchants, selected):
|
|
53
|
+
series={entry.get('id'):entry for entry in facts.get('merchant_weeks',[])}
|
|
54
|
+
blocks=[]
|
|
55
|
+
if not series:
|
|
56
|
+
return blocks+[para('尚未生成子商户周数据,请先运行最新计算脚本。','callout')]
|
|
57
|
+
grid=next(iter(series.values()))['weeks']
|
|
58
|
+
def label(week):
|
|
59
|
+
return week['start']+' 至 '+week['end']+('(不完整周)' if week['partial_week'] else '(完整周)')
|
|
60
|
+
rows=[]
|
|
61
|
+
for merchant in merchants:
|
|
62
|
+
weeks=series.get(merchant.get('id'),{}).get('weeks',[])
|
|
63
|
+
cells=[]
|
|
64
|
+
for week in weeks:
|
|
65
|
+
metrics=week['metrics']
|
|
66
|
+
if not metrics.get('layer_observed',{}).get('auth',False):
|
|
67
|
+
cells.append('')
|
|
68
|
+
else:
|
|
69
|
+
successful_usd=amount(metrics,'approved_amount')
|
|
70
|
+
cells.append(number(metrics['approvals'])+' / '+number(metrics['attempts'])+' · '+percent(metrics['auth_rate'])+(' · 授权成功 USD '+successful_usd if successful_usd else ''))
|
|
71
|
+
rows.append([name(merchant),merchant.get('id') or '未归属',*cells])
|
|
72
|
+
blocks.append(para('周汇总格里的美元是授权成功金额,不是销售金额。结算层未观察到时,销售金额留空,授权成功金额仍可出现。','note'))
|
|
73
|
+
blocks.append(table(['子商户','商户 ID',*[label(w) for w in grid]],rows,wrap_headers=True))
|
|
74
|
+
chosen={m.get('id') for m in selected}
|
|
75
|
+
for merchant in merchants:
|
|
76
|
+
weeks=series.get(merchant.get('id'),{}).get('weeks',[])
|
|
77
|
+
metric_labels=['授权尝试','授权成功笔数','授权成功率','授权成功金额 USD','较上周(百分点)','尝试笔数周环比','销售金额 USD','销售金额周环比','退款笔数','退款金额 USD','争议快照记录']
|
|
78
|
+
weekly_columns=[]
|
|
79
|
+
for week in weeks:
|
|
80
|
+
metric=week['metrics'];comparison=week['comparison'];observed=metric.get('layer_observed',{}).get('auth',False)
|
|
81
|
+
pp=comparison.get('auth_rate_pp')
|
|
82
|
+
weekly_columns.append([number(metric['attempts']) if observed else '',number(metric['approvals']) if observed else '',percent(metric['auth_rate']),amount(metric,'approved_amount'),('' if pp is None else f'{pp:+.2f}'),percent(comparison.get('attempts_pct')),amount(metric,'sales'),percent(comparison.get('sales_usd_pct')),number(metric['refund_count']) if metric['layer_observed']['settlement'] else '',amount(metric,'refunds',True),number(metric['dispute_snapshots']) if metric['layer_observed']['dispute'] else ''])
|
|
83
|
+
weekly_rows=[[metric_label,*[column[i] for column in weekly_columns]] for i,metric_label in enumerate(metric_labels)]
|
|
84
|
+
blocks.append(table(['指标',*[label(w) for w in weeks]],weekly_rows,wrap_headers=True,collapsed=merchant.get('id') not in chosen,caption=name(merchant)+' · '+str(merchant.get('id') or '未归属')+' · 每周明细'))
|
|
85
|
+
if merchant.get('id') in chosen:
|
|
86
|
+
# Visible tables need a merchant heading; collapsed ones have a summary caption.
|
|
87
|
+
blocks.insert(len(blocks)-1,para(name(merchant)+' · '+str(merchant.get('id') or '未归属')+' · 每周明细','heading'))
|
|
88
|
+
return blocks
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_weekly_csv(facts, output):
|
|
92
|
+
with (output/'merchant-weekly.csv').open('w',encoding='utf-8',newline='') as handle:
|
|
93
|
+
writer=safe_writer(handle,numeric_columns={11,13,15})
|
|
94
|
+
writer.writerow(['merchant_id','merchant_name','week_start','week_end','observed_start','observed_end','days','partial_week','attempts','approvals','auth_rate','approved_amount_usd','sale_count','sales_usd','refund_count','refunds_usd','dispute_snapshots','auth_rate_change_pp','attempts_change_fraction','sales_change_fraction','comparison_reason'])
|
|
95
|
+
for merchant in facts.get('merchant_weeks',[]):
|
|
96
|
+
for week in merchant['weeks']:
|
|
97
|
+
metrics=week['metrics'];comparison=week['comparison']
|
|
98
|
+
writer.writerow([merchant.get('id'),merchant.get('name'),week['week_start'],week['week_end'],week['start'],week['end'],week['days'],week['partial_week'],metrics['attempts'] if metrics.get('layer_observed',{}).get('auth',False) else None,metrics['approvals'] if metrics.get('layer_observed',{}).get('auth',False) else None,metrics['auth_rate'],(metrics.get('approved_amount') or {}).get('usd'),metrics['sale_count'] if metrics['layer_observed']['settlement'] else None,(metrics.get('sales') or {}).get('usd'),metrics['refund_count'] if metrics['layer_observed']['settlement'] else None,(metrics.get('refunds') or {}).get('usd'),metrics['dispute_snapshots'] if metrics['layer_observed']['dispute'] else None,comparison.get('auth_rate_pp'),comparison.get('attempts_pct'),comparison.get('sales_usd_pct'),comparison.get('reason')])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def segment_charts(merchant, periods):
|
|
102
|
+
blocks=[]
|
|
103
|
+
series=[{'key':p['key'],'label':p['label']+' '+p['start']+' 至 '+p['end']} for p in periods]
|
|
104
|
+
for dimension in merchant.get('dimensions',[]):
|
|
105
|
+
decline=dimension['population']=='non_approvals'
|
|
106
|
+
histogram=dimension['key']=='amount_bucket'
|
|
107
|
+
layout={'bin_country':'paired_bars','card_product':'paired_bars','amount_bucket':'histogram_pair','decline_reason':'bars_pair','authentication':'pie_pair'}.get(dimension['key'])
|
|
108
|
+
metrics=[{'key':'count','label':'未成功笔数' if decline else '授权尝试','format':'count'},
|
|
109
|
+
{'key':'share','label':'占该商户未成功记录比例' if decline else '占该商户授权尝试比例','format':'percent'}]
|
|
110
|
+
if not decline:
|
|
111
|
+
metrics.extend([{'key':'approvals','label':'授权成功笔数','format':'count'},{'key':'auth_rate','label':'授权成功率','format':'percent'}])
|
|
112
|
+
metrics.append({'key':'attempt_amount','label':'未成功金额 USD' if decline else '授权尝试金额 USD','format':'usd'})
|
|
113
|
+
if not decline:metrics.append({'key':'approved_amount','label':'授权成功金额 USD','format':'usd'})
|
|
114
|
+
if histogram or dimension['key']=='authentication':metrics=[metric for metric in metrics if metric['format']!='percent']
|
|
115
|
+
if not any((period['metrics'].get('count') or 0)>0 for group in dimension['groups'] for period in group['periods']):
|
|
116
|
+
continue
|
|
117
|
+
categories=[]
|
|
118
|
+
for group in dimension['groups']:
|
|
119
|
+
key=json.loads(group['key'])
|
|
120
|
+
if not (histogram and key[0]=='bucket') and not any((p['metrics'].get('count') or 0)>0 for p in group['periods']):
|
|
121
|
+
continue
|
|
122
|
+
label=('未知(缺失或不可换算)' if dimension['key']=='amount_bucket' else '未知(缺失)') if key==['unknown'] else '其他(合并)' if key==['other'] else group['label']
|
|
123
|
+
values={}
|
|
124
|
+
for period in group['periods']:
|
|
125
|
+
data=period['metrics']
|
|
126
|
+
values[period['key']]={metric['key']:((data.get(metric['key']) or {}).get('usd') if metric['format']=='usd' else data.get(metric['key'])) for metric in metrics}
|
|
127
|
+
categories.append({'label':label,'values':values,'is_unknown':key==['unknown']})
|
|
128
|
+
if not categories:
|
|
129
|
+
blocks.append(para(dimension['label']+':两期均未观察到可细分记录。','note'))
|
|
130
|
+
continue
|
|
131
|
+
chart={'type':'comparison_chart','title':dimension['label'],'series':series,'metrics':metrics,'default_metric':'count' if decline or histogram or dimension['key']=='authentication' else 'auth_rate','categories':categories}
|
|
132
|
+
if layout:chart['layout']=layout
|
|
133
|
+
blocks.append(chart)
|
|
134
|
+
return blocks
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def merchant_detail_blocks(merchant, periods):
|
|
138
|
+
return segment_charts(merchant,periods)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def risk_charts(history):
|
|
142
|
+
if not history:
|
|
143
|
+
return []
|
|
144
|
+
disputes=[{'key':'visa_predispute','label':'Visa Predispute'},
|
|
145
|
+
{'key':'visa_dispute','label':'Visa Dispute'},
|
|
146
|
+
{'key':'mastercard_chargeback','label':'Mastercard chargeback'}]
|
|
147
|
+
fraud=[{'key':'visa','label':'Visa'},{'key':'mastercard','label':'Mastercard'}]
|
|
148
|
+
if any((p.get('values',{}).get('other') or 0)>0 for p in history['fraud']['monthly']):
|
|
149
|
+
fraud.append({'key':'other','label':'其他卡组织'})
|
|
150
|
+
blocks=[]
|
|
151
|
+
for kind,title,series,basis in [
|
|
152
|
+
('disputes','争议案件观察',disputes,'同阶段最早观察到的争议日期'),
|
|
153
|
+
('fraud','欺诈报告',fraud,'340FRD 文件生成日作为报告日;完全重复记录取最早报告日')]:
|
|
154
|
+
for frequency,label in [('monthly','月度'),('weekly','周度')]:
|
|
155
|
+
points=history[kind][frequency]
|
|
156
|
+
block={'type':'time_series_chart','title':title+' · '+label,'unit':'count','series':series,'points':points,'date_basis':basis}
|
|
157
|
+
if frequency=='monthly':
|
|
158
|
+
rate_series=([{**s,'label':s['label']+('(描述性)' if s['key']=='visa_predispute' else '')} for s in disputes] if kind=='disputes' else [{'key':'visa','label':'Visa 欺诈报告率'},{'key':'mastercard_efm','label':'Mastercard EFM 欺诈拒付率'}])
|
|
159
|
+
block['default_view']='count'
|
|
160
|
+
block['views']=[
|
|
161
|
+
{'key':'count','label':'笔数','unit':'count','points':points},
|
|
162
|
+
{'key':'rate','label':'比率(估算)','unit':'percent','series':rate_series,
|
|
163
|
+
'points':[{**p,'values':p.get('rates',{})} for p in points],
|
|
164
|
+
'date_basis':'按卡组织月度时间口径估算',
|
|
165
|
+
'status_note':'Visa 同月分母;Mastercard 上月分母。' if kind=='disputes' else 'Visa:欺诈报告 / 同月交易;Mastercard EFM:欺诈拒付 / 上月电商交易。'}]
|
|
166
|
+
if kind=='fraud' and history['fraud'].get('status')=='no_valid_files':
|
|
167
|
+
block['status_note']='未提供有效欺诈文件'
|
|
168
|
+
blocks.append(block)
|
|
169
|
+
blocks.append({'type':'time_series_chart','title':'VAMP · 月度','unit':'percent','series':[{'key':'vamp_ratio','label':'VAMP'}],'points':history['vamp']['monthly'],'status_note':history['vamp'].get('reason') or ''})
|
|
170
|
+
return blocks
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def write_risk_csv(facts, output):
|
|
174
|
+
with (output/'merchant-risk-history.csv').open('w',encoding='utf-8-sig',newline='') as handle:
|
|
175
|
+
writer=safe_writer(handle,numeric_columns={7,10,11})
|
|
176
|
+
writer.writerow(['merchant_id','merchant_name','metric','frequency','start','end','series','value','date_basis','status','numerator','denominator','denominator_start','denominator_end','formula','reason'])
|
|
177
|
+
for merchant in facts.get('merchant_risk_history',[]):
|
|
178
|
+
for metric in ('disputes','fraud','vamp'):
|
|
179
|
+
history=merchant[metric]
|
|
180
|
+
for frequency in ('monthly','weekly'):
|
|
181
|
+
for point in history.get(frequency,[]):
|
|
182
|
+
for series,value in point['values'].items():
|
|
183
|
+
writer.writerow([merchant.get('id'),merchant.get('name'),metric,frequency,point['start'],point['end'],series,value,history.get('basis',''),history.get('status',''),'','','','','',''])
|
|
184
|
+
for series,value in point.get('rates',{}).items():
|
|
185
|
+
detail=point.get('rate_details',{}).get(series,{})
|
|
186
|
+
writer.writerow([merchant.get('id'),merchant.get('name'),metric+'_rate',frequency,point['start'],point['end'],series,value,detail.get('date_basis',''),detail.get('status',''),detail.get('numerator'),detail.get('denominator'),detail.get('denominator_start'),detail.get('denominator_end'),detail.get('formula'),detail.get('reason')])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def write_segment_csv(facts, output):
|
|
190
|
+
with (output/'merchant-segments.csv').open('w',encoding='utf-8',newline='') as handle:
|
|
191
|
+
writer=safe_writer(handle,numeric_columns={14,15})
|
|
192
|
+
writer.writerow(['merchant_id','merchant_name','dimension','population','group_key','group_label','period_kind','start','end','partial_week','count','approvals','auth_rate','share','attempt_amount_usd','approved_amount_usd'])
|
|
193
|
+
for merchant in facts.get('merchant_breakdowns',[]):
|
|
194
|
+
for dimension in merchant['dimensions']:
|
|
195
|
+
for group in dimension['groups']:
|
|
196
|
+
periods=[{'kind':'main','start':facts['metadata']['main_start'],'end':facts['metadata']['main_end'],'partial_week':None,'metrics':group['metrics']},*[dict(w,kind='week') for w in group['weeks']]]
|
|
197
|
+
for period in periods:
|
|
198
|
+
metric=period['metrics']
|
|
199
|
+
writer.writerow([merchant['id'],merchant['name'],dimension['key'],dimension['population'],group['key'],group['label'],period['kind'],period['start'],period['end'],period['partial_week'],metric['count'],metric['approvals'],metric['auth_rate'],metric['share'],(metric.get('attempt_amount') or {}).get('usd'),(metric.get('approved_amount') or {}).get('usd')])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def write_detail_csv(facts, output):
|
|
203
|
+
with (output/'merchant-detail-comparison.csv').open('w',encoding='utf-8',newline='') as handle:
|
|
204
|
+
writer=safe_writer(handle,numeric_columns={13,14})
|
|
205
|
+
writer.writerow(['merchant_id','merchant_name','dimension','population','group_key','group_label','period','start','end','count','approvals','auth_rate','share','attempt_amount_usd','approved_amount_usd'])
|
|
206
|
+
for merchant in facts.get('merchant_detail_comparison',{}).get('merchants',[]):
|
|
207
|
+
for dimension in merchant['dimensions']:
|
|
208
|
+
for group in dimension['groups']:
|
|
209
|
+
for period in group['periods']:
|
|
210
|
+
metric=period['metrics']
|
|
211
|
+
writer.writerow([merchant['id'],merchant['name'],dimension['key'],dimension['population'],group['key'],group['label'],period['key'],period['start'],period['end'],metric['count'],metric['approvals'],metric['auth_rate'],metric['share'],(metric.get('attempt_amount') or {}).get('usd'),(metric.get('approved_amount') or {}).get('usd')])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def without_merchants(facts, excluded):
|
|
215
|
+
if not excluded:
|
|
216
|
+
return facts
|
|
217
|
+
def keep(item):
|
|
218
|
+
return item.get('id') not in excluded
|
|
219
|
+
out=dict(facts)
|
|
220
|
+
out['merchants']=[m for m in facts.get('merchants',[]) if keep(m)]
|
|
221
|
+
out['merchant_weeks']=[m for m in facts.get('merchant_weeks',[]) if keep(m)]
|
|
222
|
+
out['merchant_risk_history']=[m for m in facts.get('merchant_risk_history',[]) if keep(m)]
|
|
223
|
+
out['merchant_breakdowns']=[m for m in facts.get('merchant_breakdowns',[]) if keep(m)]
|
|
224
|
+
detail=facts.get('merchant_detail_comparison') or {}
|
|
225
|
+
out['merchant_detail_comparison']={**detail,'merchants':[m for m in detail.get('merchants',[]) if keep(m)]}
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def filter_existing_csv(path, excluded):
|
|
230
|
+
if not path.exists() or not excluded:
|
|
231
|
+
return
|
|
232
|
+
with path.open(newline='',encoding='utf-8-sig') as handle:
|
|
233
|
+
rows=list(csv.reader(handle))
|
|
234
|
+
if not rows:
|
|
235
|
+
return
|
|
236
|
+
header,body=rows[0],rows[1:]
|
|
237
|
+
id_at=next((i for i,cell in enumerate(header) if cell in ('merchant_id','id')),None)
|
|
238
|
+
kept=[row for row in body if (row[id_at] if id_at is not None and id_at<len(row) else None) not in excluded and not any(cell in excluded for cell in row)]
|
|
239
|
+
with path.open('w',newline='',encoding='utf-8') as handle:
|
|
240
|
+
csv.writer(handle).writerows([header,*kept])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build(facts, detail_merchants=None, excluded_ids=None):
|
|
244
|
+
excluded=set(excluded_ids or [])
|
|
245
|
+
known={m.get('id') for m in facts.get('merchants',[])}
|
|
246
|
+
unknown=excluded-known
|
|
247
|
+
if unknown:
|
|
248
|
+
raise ValueError('Unknown excluded merchant IDs: '+', '.join(sorted(unknown)))
|
|
249
|
+
excluded_merchants=[m for m in facts.get('merchants',[]) if m.get('id') in excluded]
|
|
250
|
+
facts=without_merchants(facts,excluded)
|
|
251
|
+
meta=facts['metadata']
|
|
252
|
+
merchants=sorted(facts['merchants'],key=lambda m:(-m['attempts'],m.get('id') or ''))
|
|
253
|
+
active=[m for m in merchants if m['attempts']]
|
|
254
|
+
eligible=[m for m in active if m['attempts']>=30 and m['auth_rate'] is not None]
|
|
255
|
+
lowest=min(eligible,key=lambda m:m['auth_rate']) if eligible else None
|
|
256
|
+
followups=[m for m in merchants if not m['attempts'] and ((m['refund_count'] or 0)>0 or (m['dispute_snapshots'] or 0)>0)]
|
|
257
|
+
selected=active[:1]
|
|
258
|
+
if lowest and lowest not in selected:
|
|
259
|
+
selected.append(lowest)
|
|
260
|
+
if detail_merchants:
|
|
261
|
+
requested=set(detail_merchants)
|
|
262
|
+
known={m.get('id') for m in merchants}
|
|
263
|
+
if not requested.issubset(known):
|
|
264
|
+
raise ValueError('Unknown detail merchant IDs: '+', '.join(sorted(requested-known)))
|
|
265
|
+
selected=[m for m in merchants if m.get('id') in requested]
|
|
266
|
+
findings=[]
|
|
267
|
+
if lowest:
|
|
268
|
+
findings.append(f"授权核查可从 {name(lowest)} 开始:在授权尝试不少于 30 笔的子商户中,其授权成功率最低,为 {percent(lowest['auth_rate'])}({number(lowest['approvals'])} / {number(lowest['attempts'])})。商户间的国家和卡种差异尚未控制,该排名只用于挑选授权核查对象,不是已确认原因。")
|
|
269
|
+
if active:
|
|
270
|
+
largest=active[0]
|
|
271
|
+
findings.append(f"{name(largest)} 的授权尝试最多,为 {number(largest['attempts'])} 笔,授权成功率为 {percent(largest['auth_rate'])}。详细分析其拒绝响应和发卡行国家,有助于确定需要核查的交易群体。")
|
|
272
|
+
if followups:
|
|
273
|
+
findings.append('本期未观察到授权、但仍有退款或争议记录的子商户包括 '+ '、'.join(name(m) for m in followups)+'。应核对历史交易和后续处理责任,不能据此认定商户已经停业。')
|
|
274
|
+
if excluded_merchants:
|
|
275
|
+
findings.append('、'.join(name(m)+'('+str(m.get('id'))+')' for m in excluded_merchants)+'已按历史商户排除,不进入本期对比、明细和核查清单。排除不是停业结论,也不是数据缺失。')
|
|
276
|
+
if not findings:
|
|
277
|
+
findings.append('本期没有足够的子商户授权记录用于比较。请先核对各商户的数据覆盖与归属。')
|
|
278
|
+
days=(date.fromisoformat(meta['main_end'])-date.fromisoformat(meta['main_start'])).days+1
|
|
279
|
+
model={
|
|
280
|
+
'title':'子商户支付表现分析',
|
|
281
|
+
'subtitle':'根据 Worldpay 授权、结算、争议和欺诈文件整理 · 比较子商户表现,定位商户问题,明确逐商户核查事项。',
|
|
282
|
+
'periods':[f"报告期:{meta['main_start']} 至 {meta['main_end']}({days} 天)",'时间口径:保留源文件日期;时区未验证'],
|
|
283
|
+
'summary_title':'子商户核查重点','summary':findings,'sections':[],
|
|
284
|
+
'links':[{'label':'商户风险历史 CSV','file':'merchant-risk-history.csv'},{'label':'本周与上周同期细分 CSV','file':'merchant-detail-comparison.csv'},{'label':'子商户每周指标 CSV','file':'merchant-weekly.csv'},{'label':'子商户指标 CSV','file':'merchant-metrics.csv'},{'label':'商户归属 CSV','file':'merchant-mapping.csv'},{'label':'归属与核验记录 JSON','file':'coverage-and-reconciliation.json'},{'label':'Markdown 报告','file':'overview.zh.md'}],
|
|
285
|
+
'footer':'来源:Worldpay 授权、结算、争议及 DART-340 欺诈文件。'}
|
|
286
|
+
sections=model['sections']
|
|
287
|
+
comparison=[]
|
|
288
|
+
for m in merchants:
|
|
289
|
+
status='本期有授权记录' if m['attempts'] else '本期无授权记录'
|
|
290
|
+
if 0<m['attempts']<30:status='样本少于 30 笔'
|
|
291
|
+
observed=m.get('layer_observed',{}).get('auth',False)
|
|
292
|
+
comparison.append([name(m),m.get('id') or '未归属',number(m['attempts']) if observed else '',number(m['approvals']) if observed else '',percent(m['auth_rate']),amount(m,'sales'),number(m['refund_count']),amount(m,'refunds',True),number(m['dispute_snapshots']),status])
|
|
293
|
+
sections.append({'title':'1. 子商户对比','nav':'子商户对比','blocks':[
|
|
294
|
+
table(['子商户','商户 ID','授权尝试','授权成功','授权成功率','销售金额 USD','退款笔数','退款金额 USD','争议快照记录','样本说明'],comparison,filter=True),
|
|
295
|
+
]})
|
|
296
|
+
sections.append({'title':'2. 子商户逐周对比','nav':'逐周对比','blocks':weekly_blocks(facts,merchants,selected)})
|
|
297
|
+
detail_groups=[]
|
|
298
|
+
comparison_data=facts.get('merchant_detail_comparison',{})
|
|
299
|
+
periods=comparison_data.get('periods',[])
|
|
300
|
+
comparisons={item.get('id'):item for item in comparison_data.get('merchants',[])}
|
|
301
|
+
histories={item.get('id'):item for item in facts.get('merchant_risk_history',[])}
|
|
302
|
+
expanded={m.get('id') for m in selected}
|
|
303
|
+
for m in merchants:
|
|
304
|
+
comparison=comparisons.get(m.get('id'))
|
|
305
|
+
blocks=merchant_detail_blocks(comparison,periods) if comparison else []
|
|
306
|
+
blocks.extend(risk_charts(histories.get(m.get('id'))))
|
|
307
|
+
detail_groups.append({'type':'group','caption':name(m)+' · '+str(m.get('id') or '未归属'),'collapsed':m.get('id') not in expanded,'blocks':blocks})
|
|
308
|
+
sections.append({'title':'3. 重点子商户详情','nav':'商户详情','blocks':detail_groups})
|
|
309
|
+
action_rows=[]
|
|
310
|
+
for m in selected:
|
|
311
|
+
reasons=top_reasons(m)
|
|
312
|
+
if reasons:
|
|
313
|
+
reason=reasons[0]
|
|
314
|
+
evidence=f"{reason['message']}:{number(reason['count'])} 笔;授权成功率 {percent(m['auth_rate'])}"
|
|
315
|
+
action='核对该响应在该商户下的处理商含义,再按发卡行国家和认证状态细分检查。'
|
|
316
|
+
else:
|
|
317
|
+
evidence=f"授权成功 {number(m['approvals'])} / {number(m['attempts'])};缺少可展开的拒绝响应"
|
|
318
|
+
action='核对该商户的响应字段与来源覆盖,避免根据缺失原因直接调整策略。'
|
|
319
|
+
action_rows.append([name(m),m.get('id') or '未归属',evidence,action,'不将观察差异视为已确认原因,不估算可挽回金额。'])
|
|
320
|
+
for m in followups:
|
|
321
|
+
action_rows.append([name(m),m.get('id') or '未归属',f"未观察到本期授权;退款 {number(m['refund_count'])} 笔,争议快照记录 {number(m['dispute_snapshots'])} 条",'核对原始销售所属商户、退款处理及争议责任。','无本期授权不代表停业;争议记录不等于新增案件。'])
|
|
322
|
+
if not action_rows:
|
|
323
|
+
action_rows=[['未归属或缺少观察','','缺少支持逐商户判断的记录','先补齐商户身份及来源覆盖。','暂不形成商户表现结论。']]
|
|
324
|
+
sections.append({'title':'4. 逐商户核查清单','nav':'核查清单','blocks':[table(['子商户','商户 ID','观察依据','下一步核查','解释边界'],action_rows)]})
|
|
325
|
+
coverage=[table(['子商户','商户 ID','授权记录','结算记录','争议记录'],[[name(m),m.get('id') or '未归属',*['✓' if m.get('layer_observed',{}).get(layer,False) else '' for layer in ('auth','settlement','dispute')]] for m in merchants])]
|
|
326
|
+
audit=facts.get('quality',{}).get('risk_history',{})
|
|
327
|
+
if audit:
|
|
328
|
+
attribution=audit.get('fraud_attribution',{})
|
|
329
|
+
files=audit.get('files',[])
|
|
330
|
+
invalid=[item.get('date',item['file']) for item in files if item.get('status')=='invalid']
|
|
331
|
+
future=[item.get('date',item['file']) for item in files if item.get('status')=='future_not_parsed']
|
|
332
|
+
unmatched=attribution.get('unmatched')
|
|
333
|
+
amounts=audit.get('unmatched_amounts_by_currency') or {}
|
|
334
|
+
rows=[
|
|
335
|
+
['欺诈记录归属',str(attribution.get('attributed',0))+' / '+str(audit.get('distinct_exact_rows',0))],
|
|
336
|
+
['无效欺诈文件日期','、'.join(invalid) or '无'],
|
|
337
|
+
['数据截至',meta['main_end']]
|
|
338
|
+
]
|
|
339
|
+
if future:
|
|
340
|
+
rows.append(['晚于截止日、未解析的欺诈文件','、'.join(future)])
|
|
341
|
+
if unmatched:
|
|
342
|
+
amount_text='、'.join(currency+' '+money(value) for currency,value in amounts.items())
|
|
343
|
+
rows.append(['未归属欺诈记录',str(unmatched)+' 条'+((',金额 '+amount_text) if amount_text else '')+',未计入商户笔数'])
|
|
344
|
+
coverage.append(table(['数据范围','观察值'],rows))
|
|
345
|
+
notes=[]
|
|
346
|
+
if any(item.get('error')=='missing_header' for item in files):
|
|
347
|
+
notes.append('缺少表头的欺诈文件未解析,因此没有进入笔数。')
|
|
348
|
+
if future:
|
|
349
|
+
notes.append('晚于截止日的欺诈文件未解析。')
|
|
350
|
+
if unmatched:
|
|
351
|
+
notes.append('未能归属的欺诈记录不计入商户笔数。')
|
|
352
|
+
definitions_note=''.join(notes)
|
|
353
|
+
else:
|
|
354
|
+
definitions_note=''
|
|
355
|
+
definitions=[
|
|
356
|
+
para('空白为不可用或未观察到的数据,不作为 0;0 仅表示提供的记录中观察到的零值。'),
|
|
357
|
+
para('授权、销售与退款按各自事件日期统计。授权成功金额与结算销售金额不同,退款金额以绝对值展示。周汇总格里的美元是授权成功金额,不是销售金额。销售金额 0.00 是结算层观察到的零值;没有结算层记录时销售金额留空。'),
|
|
358
|
+
para('争议趋势按同一阶段最早观察到的争议日期统计案件,状态更新不重复计数;并非新增案件。欺诈按 340FRD 文件生成日作为报告日,完全重复记录取最早报告日。'),
|
|
359
|
+
para('比率按卡组织月度时间口径作提取数据估算,并非正式监控结果。Visa 使用当月分母,Mastercard 拒付与 EFM 使用上月分母;EFM 分子是 4837/4863 欺诈拒付,不是 340FRD 的全部 Mastercard 报告。周图仅展示笔数;预争议比率为描述性指标,不纳入 VAMP。'),
|
|
360
|
+
para('340FRD 报告日已按文件生成日确认。'+definitions_note+'VAMP 仍需核验 TC15/TC05 处理日、事件身份、适用范围及排除标记。')
|
|
361
|
+
]
|
|
362
|
+
coverage.append({'type':'group','caption':'数据口径','collapsed':True,'blocks':definitions})
|
|
363
|
+
sections.append({'title':'5. 商户归属与覆盖','nav':'归属与覆盖','blocks':coverage})
|
|
364
|
+
return model
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__=='__main__':
|
|
368
|
+
parser=argparse.ArgumentParser(description=__doc__)
|
|
369
|
+
parser.add_argument('--facts',type=Path,required=True)
|
|
370
|
+
parser.add_argument('--output',type=Path,required=True)
|
|
371
|
+
parser.add_argument('--detail-merchant',action='append',help='Merchant ID to expand; repeat to select several')
|
|
372
|
+
parser.add_argument('--exclude-merchant',action='append',help='Merchant ID to omit as historical; repeat to exclude several')
|
|
373
|
+
args=parser.parse_args()
|
|
374
|
+
facts=json.loads(args.facts.read_text(encoding='utf-8'))
|
|
375
|
+
excluded=set(args.exclude_merchant or [])
|
|
376
|
+
model=build(facts,args.detail_merchant,excluded)
|
|
377
|
+
report_facts=without_merchants(facts,excluded)
|
|
378
|
+
args.output.mkdir(parents=True,exist_ok=True)
|
|
379
|
+
write_weekly_csv(report_facts,args.output)
|
|
380
|
+
write_segment_csv(report_facts,args.output)
|
|
381
|
+
write_detail_csv(report_facts,args.output)
|
|
382
|
+
write_risk_csv(report_facts,args.output)
|
|
383
|
+
for filename in ('merchant-metrics.csv','merchant-mapping.csv'):
|
|
384
|
+
filter_existing_csv(args.output/filename,excluded)
|
|
385
|
+
(args.output/'presentation.json').write_text(json.dumps(model,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
|
386
|
+
render(model,args.output)
|